<?php
namespace App\Bus;
use App\Entity\Bus\BookingMpesa;
use App\Entity\Bus\BookingSeat;
use App\Entity\Bus\BusLayout;
use App\Entity\Bus\Reservation;
use App\Entity\Bus\RouteFareRule;
use App\Entity\Bus\Seat;
use App\Entity\Bus\Stop;
use App\Entity\Bus\Trip;
use App\Entity\Bus\TripVehicle;
use App\Entity\Mpesa;
use App\Entity\MpesaAuth;
use App\Entity\MpesaPayment;
use App\Entity\MpesaPaymentRequest;
use App\Entity\MpesaResponse;
use App\Entity\User;
use App\Entity\UserStation;
use App\Entity\Vehicle;
use App\Entity\WayBill;
use App\Form\Bus\BookingType;
use App\Form\Bus\ReserveType;
use App\Form\Bus\RouteeType;
use App\Form\Bus\TripType;
use App\Service\SeatHoldReleaser;
use DateTime;
use Doctrine\DBAL\Exception;
use Doctrine\Persistence\ManagerRegistry;
use Doctrine\Persistence\ObjectManager;
use JMS\Serializer\SerializationContext;
use JMS\Serializer\SerializerBuilder;
use Mpdf\Output\Destination;
use Mpdf\QrCode\Output\Png;
use Mpdf\QrCode\QrCode;
use PhpOffice\PhpSpreadsheet\Worksheet\AutoFilter\Column\Rule;
use Sasedev\MpdfBundle\Factory\MpdfFactory;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\Form\FormError;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
class BookingController extends AbstractController{
private ManagerRegistry $registry;
private ObjectManager $entityManager;
private RequestStack $requestStack;
private SeatHoldReleaser $seatHoldReleaser;
public function __construct(ManagerRegistry $registry, RequestStack $requestStack, SeatHoldReleaser $seatHoldReleaser)
{
$this->entityManager = $registry->getManager();
$this->requestStack = $requestStack;
$this->seatHoldReleaser = $seatHoldReleaser;
}
/** @Route("/routes", name="routesHome") */
public function routesHome(Request $request)
{
$em = $this->entityManager;
/** @var UserStation $userTown */
$userTown = $em->getRepository(UserStation::class)->findOneBy([
'user' => $this->getUser(),
'isActive' => true
], ['id' => 'DESC']);
$route = new \App\Entity\Bus\Route();
$route->setCreatedAt(new DateTime());
$route->setCreatedBy($this->getUser());
$routeForm = $this->createForm(RouteeType::class, $route);
$routeForm->handleRequest($request);
if ($routeForm->isSubmitted() && $routeForm->isValid()) {
dump($route);
$this->entityManager->persist($route);
$this->entityManager->flush();
return $this->redirectToRoute('routesHome');
}
/** @var \App\Entity\Bus\Route[] $routes */
$routes = $this->entityManager->getRepository(\App\Entity\Bus\Route::class)->findAll();
return $this->render('bus/routes.html.twig', [
'user_town' => $userTown,
'routes' => $routes,
'routeForm' => $routeForm->createView()
]);
}
/** @Route("/routes/{route}/edit", name="routesEdit") */
public function routesEdit(Request $request, \App\Entity\Bus\Route $route)
{
$em = $this->entityManager;
/** @var UserStation $userTown */
$userTown = $em->getRepository(UserStation::class)->findOneBy([
'user' => $this->getUser(),
'isActive' => true
], ['id' => 'DESC']);
$routeForm = $this->createForm(RouteeType::class, $route);
$routeForm->handleRequest($request);
if ($routeForm->isSubmitted() && $routeForm->isValid()) {
// $route->setStops($route->getStops());
$this->entityManager->persist($route);
$this->entityManager->flush();
$this->addFlash('success', 'Route updated successfully');
return $this->redirectToRoute('routesHome');
}
/** @var \App\Entity\Bus\Route[] $routes */
$routes = $this->entityManager->getRepository(\App\Entity\Bus\Route::class)->findAll();
return $this->render('bus/routes_edit.html.twig', [
'user_town' => $userTown,
'routes' => $routes,
'routeForm' => $routeForm->createView()
]);
}
/** @Route("/", name="tripsHome") */
public function tripsHome(Request $request)
{
$em = $this->entityManager;
$date = $request->get('trip_date');
$today = new \DateTimeImmutable();
if (!$date) {
$date = $today->format('Y-m-d');
}
/** @var UserStation $userTown */
$userTown = $em->getRepository(UserStation::class)->findOneBy([
'user' => $this->getUser(),
'isActive' => true
], ['id' => 'DESC']);
$stationId = $userTown->getStation()->getId();
// MOMBASA and MERU scenario 1
/* if a station origin is in current station show bus route */
// $sql = "SELECT a.id, a.route_name FROM bus_route a WHERE origin = {$stationId}";
// EMBU scenario 2
/* if a station is a stop in a main route and not the destination */
$sql = "SELECT DISTINCT a.id, a.route_name FROM bus_route a
LEFT JOIN bus_stop b ON b.route_id = a.id AND b.route_type = 'MAIN' AND b.station_id = {$stationId}
WHERE (b.station_id = {$stationId} OR a.origin = {$stationId})
AND a.destination != {$stationId}";
$conn = $this->entityManager->getConnection();
$availableRoutes = null;
try {
$stmt = $conn->prepare($sql);
$availableRoutes = $stmt->executeQuery()->fetchAllAssociative();
} catch (\Exception $e) {
}
/** @var Trip[] $trips */
$trips = $this->entityManager->getRepository(Trip::class)->findActiveTrips($date, $userTown->getStation()->getId(), 'MAIN');
if($trips){
return $this->render('bus/trips.html.twig', [
'user_town' => $userTown,
'trips' => $trips,
'date' => $date
]);
}else {
foreach ($availableRoutes as $index => $availableRoute) {
$route = $em->getRepository(\App\Entity\Bus\Route::class)->findOneBy([
'id' => $availableRoute['id']
]);
$this->addTrip($route, $date);
}
}
/** @var Trip[] $trips */
$trips = $this->entityManager->getRepository(Trip::class)->findActiveTrips($date, $userTown->getStation()->getId(), 'MAIN');
return $this->render('bus/trips.html.twig', [
'user_town' => $userTown,
'trips' => $trips,
'date' => $date
]);
}
/** @Route("/trips/new", name="newTrip") */
public function newTrip(Request $request)
{
$em = $this->entityManager;
/** @var UserStation $userTown */
$userTown = $em->getRepository(UserStation::class)->findOneBy([
'user' => $this->getUser(),
'isActive' => true
], ['id' => 'DESC']);
$trip = new Trip();
$tripForm = $this->createForm(TripType::class, $trip);
$tripForm->handleRequest($request);
if ($tripForm->isSubmitted() && $tripForm->isValid()) {
$date = $trip->getTripDate();
$d = $date->format('Y-m-d');
$sql = "SELECT count(*) total FROM bus_trip a WHERE a.route_id = {$trip->getRoute()->getId()}
AND DATE(a.trip_date) = CAST('{$d}' as date)";
$conn = $this->entityManager->getConnection();
$stmt = $conn->prepare($sql);
$thisDaysTrips = $stmt->executeQuery()->fetchAssociative();
$sameRouteTrips = $thisDaysTrips['total'];
$trip->setTripIndex($sameRouteTrips + 1);
$trip->setCreatedBy($this->getUser());
$trip->setCreatedAt(new DateTime());
/** @var Vehicle $vehicle */
$vehicle = $tripForm->get('vehicle')->getData();
$seats = [];
for ($i = 1; $i <= $vehicle->getPassengerCapacity(); $i++) {
$seat = new Seat();
$seat->setStatus('AVAILABLE');
$seat->setSeatNumber($i);
$seat->setTrip($trip);
$seat->setIsBooked(false);
$seat->setCreatedAt(new DateTime());
$seats[] = $seat;
}
$trip->setSeats($seats);
$tripVehicle = new TripVehicle();
$tripVehicle->setCreatedAt(new DateTime());
$tripVehicle->setTrip($trip);
$tripVehicle->setCreatedBy($this->getUser());
$tripVehicle->setIsCancelled(false);
$tripVehicle->setVehicle($vehicle);
// $this->entityManager->persist($tripVehicle);
// $this->entityManager->persist($trip);
// $this->entityManager->flush();
return $this->redirectToRoute('tripsHome');
}
return $this->render('bus/new_trip.html.twig', [
'tripForm' => $tripForm->createView(),
'user_town' => $userTown
]);
}
/** @Route("/trip/{trip}", name="viewTrip") */
public function bookingTrip(Request $request, Trip $trip)
{
$currentSeat = $request->get('currentSeat') ? $request->get('currentSeat') : -1;
$layout = $this->entityManager->getRepository(BusLayout::class)->findOneBy(['isDefault' => true]);
$doorRow = $layout ? ($layout->getDoorRow() ?? 0) : 0;
$lastRow = $layout ? max(0, $layout->getNumRows() - 1) : 12;
$rows = 0;
/** @var UserStation $userTown */
$userTown = $this->entityManager->getRepository(UserStation::class)->findOneBy([
'user' => $this->getUser(),
'isActive' => true
], ['id' => 'DESC']);
// dump($userTown);
$stop = $this->entityManager->getRepository(Stop::class)->findCurrentStop($trip->getRoute(), $userTown->getStation());
// dump($stop);
// die;
$currentStop = null;
if ($stop) {
$currentStop = $this->entityManager->getRepository(Stop::class)->findOneBy([
'id' => $stop['id']
]);
}
if (!$currentStop) {
return $this->redirectToRoute('tripsHome');
}
// Self-heal any abandoned online holds before rendering, same as
// the customer-facing seat map (BusApi\TripController::trip()) -
// otherwise a seat whose hold expired minutes ago would still show
// here as reserved until someone else happens to try booking it.
$releasedCount = $this->seatHoldReleaser->releaseAllStaleForTrip($trip);
if ($releasedCount > 0) {
$this->entityManager->flush();
}
$seats = $this->entityManager->getRepository(BookingSeat::class)->findAllSeats($trip, $currentStop);
// dump($seats);
// dump('currentSeat',$currentSeat);
return $this->render('bus/booking.html.twig', [
'seats' => $seats,
'rows' => $rows,
'doorRow' => $doorRow,
'lastRow' => $lastRow,
'workingSeat' => $currentSeat,
'trip' => $trip,
'staleHoldMinutes' => SeatHoldReleaser::staleHoldMinutes(),
]);
}
/** @Route("/trip/{trip}/json", name="viewJsonTrip") */
public function getTripSeatsJson(Request $request, Trip $trip)
{
$context = new SerializationContext();
$context->setSerializeNull(true);
$page = $request->request->get('page') > 1 ? $request->request->get('page') : 1;
$rows = $request->request->get('rows') > 1 ? $request->request->get('rows') : 50;
$offset = ($page - 1) * $rows;
$serializer = SerializerBuilder::create()->build();
$filterRules = $request->request->get('filterRules');
// Same self-heal as bookingTrip() above - this is the datagrid's
// own independent data source (initial load, reload, refilter all
// hit this action), so it needs the same check rather than relying
// on the full-page render to have already done it once.
$releasedCount = $this->seatHoldReleaser->releaseAllStaleForTrip($trip);
if ($releasedCount > 0) {
$this->entityManager->flush();
}
$seats = $this->entityManager->getRepository(BookingSeat::class)->findAllTripSeats($filterRules, $offset, $rows, $trip);
return new JsonResponse($seats);
}
/** @Route("/trip/{trip}/manifest/pdf", name="viewTripManifest") */
public function getManifestBySearch(Request $request, Trip $trip, MpdfFactory $mpdfFactory)
{
$context = new SerializationContext();
$context->setSerializeNull(true);
$page = $request->request->get('page') > 1 ? $request->request->get('page') : 1;
$rows = $request->request->get('rows') > 1 ? $request->request->get('rows') : 50;
$offset = ($page - 1) * $rows;
$filterRules = $request->request->get('filterRules');
$seats = $this->entityManager->getRepository(BookingSeat::class)->findAllTripSeats($filterRules, $offset, $rows, $trip);
$data = $this->jsonTestData();
// dump($fontData);die;
$mPdf = $mpdfFactory->createMpdfObject([
'format' => [80, 500],
'margin_header' => 5,
'margin_footer' => 5,
'margin_left' => 5,
'margin_right' => 5,
'margin_top' => 0,
'margin_bottom' => 0,
'autoPageBreak' => false,
'orientation' => 'P'
]);
$date = new DateTime();
$time = $date->getTimestamp();
try {
$receiptType = 'MANIFEST';
$logoImage = file_get_contents('../public/bus/bus_1.png');
$logoBase64 = base64_encode($logoImage);
$mPdf->WriteHTML($this->renderView('bus/manifest/manifest.html.twig', [
'passengers' => $seats,
'trip' => $trip,
'busLogo' => $logoBase64
]));
$file = "../manifests/manifest_.pdf";
// return $MpdfFactory->createDownloadResponse($mPdf, "ticket_{$ticketId}.pdf", Response::HTTP_OK, ["Set-Cookie", "fileDownload=true; path=/"]);
return $mpdfFactory->createInlineResponse($mPdf, $file);
} catch (\Exception $e) {
return new Response($e->getMessage(), Response::HTTP_BAD_REQUEST);
}
// return new JsonResponse($seats);
}
/** @Route("/trip/{trip}/stops", name="viewTripStopsSearchCombo") */
public function getTripStops(Trip $trip)
{
$em = $this->entityManager;
/** @var \App\Entity\Bus\Route $route */
$route = $em->getRepository(\App\Entity\Bus\Route::class)->findOneBy([
'id' => $trip->getRoute()->getId()
]);
$sql = "SELECT a.id,b.station_name FROM bus_stop a
JOIN station b ON b.id = a.station_id
WHERE a.route_id = {$route->getId()}";
$conn = $em->getConnection();
$stmt = $conn->prepare($sql);
$stops = $stmt->executeQuery()->fetchAllAssociative();
array_push($stops, ['id' => '', 'station_name' => 'ALL STOPS']);
return new JsonResponse($stops);
}
/** @Route("/trip/{trip}/seat/{seat}", name="viewTripSeat") */
public function bookingTripSeat(Request $request, $seat, Trip $trip)
{
$em = $this->entityManager;
/** @var Seat $busSeat */
$busSeat = $em->getRepository(Seat::class)->findOneBy([
'trip' => $trip,
'seatNumber' => $seat
]);
$seatBooking = $em->getRepository(BookingSeat::class)->findOneBy([
'seat' => $busSeat
]);
$bookingUserStation = null;
if ($seatBooking) {
/** @var UserStation $userTown */
$bookingUserStation = $this->entityManager->getRepository(UserStation::class)->findOneBy([
'user' => $seatBooking->getCreatedBy(),
'isActive' => true
], ['id' => 'DESC']);
}
/** @var User $user */
$user = $this->getUser();
/** @var UserStation $userStation */
$userStation = $em->getRepository(UserStation::class)->findOneBy([
'user' => $user,
'isActive' => true
]);
$mpesaAuth = $em->getRepository(MpesaAuth::class)->findOneBy([
'station' => $userStation->getStation(),
'shortCodeType' => 'IMANI'
]);
if (!$mpesaAuth) {
$mpesaAuth = $em->getRepository(MpesaAuth::class)->findOneBy([
'station' => 2,
'shortCodeType' => 'IMANI'
]);
}
$reserve = $em->getRepository(Reservation::class)->findOneBy([
'seat' => $busSeat
]);
$booking = new BookingSeat();
$booking->setCreatedBy($this->getUser());
$booking->setCreatedAt(new DateTime());
$booking->setSeat($busSeat);
$defaultFare = $this->getDatabaseFare($trip);
if ($defaultFare > 0) {
$booking->setAmount($defaultFare);
}
// if ($bookingUserStation) {
// $booking->setDepartureTime($bookingUserStation->getDepartureTime());
// }
$bookingForm = $this->createForm(BookingType::class, $booking, [
'trip' => $trip,
'shortCode' => $mpesaAuth->getPaybill(),
'action' => $this->generateUrl('viewTripSeat', ['seat' => $seat, 'trip' => $trip->getId()])
]);
$bookingForm->handleRequest($request);
if ($bookingForm->isSubmitted()) {
$dbFare = $this->getDatabaseFare($trip, $booking->getOrigin(), $booking->getDestination());
if ($dbFare > 0) {
$booking->setAmount($dbFare);
}
dump($request->getMethod());
if ($bookingForm->has('mpesa')) {
if ($bookingForm->get('mpesa') && $bookingForm->get('mpesa')->getData()) {
/** @var Mpesa $mpesa */
$mpesa = $bookingForm->get('mpesa')->getData();
if ($booking->getPaidVia() == 'MPESA' || $booking->getPaidVia() == 'MPESA_EXPRESS') {
dump($mpesa);
if ($booking->getAmount() > $mpesa->getTransactionAmount()) {
$bookingForm->get('amount')->addError(new FormError('Mpesa amount not enough'));
}
} else if ($booking->getPaidVia() == 'CASH_MPESA') {
if ($mpesa->getTransactionAmount() >= $booking->getAmount()) {
$bookingForm->get('amount')->addError(new FormError('Please check the amounts'));
}
}
}
}
if ($bookingForm->isValid()) {
if ($bookingForm->has('mpesa') && $bookingForm->get('mpesa') && $bookingForm->get('mpesa')->getData()) {
// dump($bookingForm->get('mpesa')->getData());
/** @var Mpesa $mpesa */
$mpesa = $bookingForm->get('mpesa')->getData();
$bookingMpesa = new BookingMpesa();
$bookingMpesa->setBooking($booking);
$bookingMpesa->setCreatedAt(new DateTime());
$bookingMpesa->setCreatedBy($this->getUser());
$bookingMpesa->setAmount($booking->getAmount());
$bookingMpesa->setBalance(0);
$bookingMpesa->setMpesa($mpesa);
$mpesa->setIsUsed(true);
$this->entityManager->persist($bookingMpesa);
}
$departureTime = $booking->getOrigin()->getDepartureTime();
$eta = $booking->getOrigin()->getEta();
$booking->setDepartureTime($departureTime);
$booking->setEta($eta);
$booking->getSeat()->setStatus('booked');
$this->entityManager->persist($booking);
$this->entityManager->flush();
// return $this->redirectToRoute("@viewTrip?currentSeat={$seat}", ['trip' => $trip->getId()]);
return $this->redirect("/bus/trip/{$trip->getId()}?currentSeat={$seat}");
}
}
return $this->render('bus/seat_detail.html.twig', [
'seat' => $busSeat,
'trip' => $trip,
'booking' => $seatBooking,
'bookingForm' => $bookingForm->createView(),
'bookingStation' => $bookingUserStation,
'reservation' => $reserve,
'user_station' => $userStation
]);
}
/** @Route("/trip/{trip}/seat/{seat}/book_reserve_opt", name="viewReserveBookOpt") */
public function bookingReserveOption(Request $request, $seat, Trip $trip)
{
$em = $this->entityManager;
/** @var Seat $busSeat */
$busSeat = $em->getRepository(Seat::class)->findOneBy([
'trip' => $trip,
'seatNumber' => $seat
]);
$reserve = $em->getRepository(Reservation::class)->findOneBy([
'seat' => $busSeat,
'canceledAt' => null
]);
$seatBooking = $em->getRepository(BookingSeat::class)->findOneBy([
'seat' => $busSeat,
]);
if ($seatBooking || $reserve) {
return $this->redirectToRoute('viewTripSeat', ['trip' => $trip->getId(), 'seat' => $busSeat->getSeatNumber()]);
}
return $this->render('bus/book_reserve.html.twig', [
'seat' => $busSeat,
'trip' => $trip,
'booking' => $seatBooking
]);
}
/** @Route("/trip/{trip}/seat/{seat}/cancel_reservation", name="cancelReservation") */
public function cancelReservation(Request $request, $seat, Trip $trip)
{
$em = $this->entityManager;
/** @var Seat $busSeat */
$busSeat = $em->getRepository(Seat::class)->findOneBy([
'trip' => $trip,
'seatNumber' => $seat
]);
$seatBooking = $em->getRepository(BookingSeat::class)->findOneBy([
'seat' => $busSeat
]);
$reserve = $em->getRepository(Reservation::class)->findOneBy([
'seat' => $busSeat,
'canceledAt' => null
]);
$busSeat->setStatus('AVAILABLE');
$reserve->setCanceledAt(new DateTime());
$reserve->setCanceledBy($this->getUser());
$em->flush();
return $this->redirectToRoute('viewTrip', ['trip' => $trip->getId()]);
}
/** @Route("/trip/{trip}/seat/{seat}/reserve", name="viewReserve") */
public function reserve(Request $request, $seat, Trip $trip)
{
$em = $this->entityManager;
/** @var UserStation $userStation */
$userStation = $em->getRepository(UserStation::class)->findOneBy([
'user' => $this->getUser(),
'isActive' => true
]);
/** @var Seat $busSeat */
$busSeat = $em->getRepository(Seat::class)->findOneBy([
'trip' => $trip,
'seatNumber' => $seat
]);
$seatBooking = $em->getRepository(BookingSeat::class)->findOneBy([
'seat' => $busSeat
]);
$reservation = new Reservation();
$reservation->setReservedAt(new DateTime());
$reservation->setReservedBy($this->getUser());
$reservation->setReservingStation($userStation->getStation());
$reservation->setSeat($busSeat);
$reserveForm = $this->createForm(ReserveType::class, $reservation);
$reserveForm->handleRequest($request);
if ($reserveForm->isSubmitted() && $reserveForm->isValid()) {
$em->persist($reservation);
$busSeat->setStatus('RESERVED');
$em->flush();
return $this->redirectToRoute('viewTrip', ['trip' => $trip->getId()]);
}
return $this->render('bus/reserve.html.twig', [
'seat' => $busSeat,
'trip' => $trip,
'booking' => $seatBooking,
'reserveForm' => $reserveForm->createView(),
'action' => $this->generateUrl('viewReserve', ['trip' => $trip->getId(), 'seat' => $busSeat->getSeatNumber()])
]);
}
/**
* @Route("/ticket/{ticketId}", methods={"GET"}, name="generateTicket")
*/
public function generateTicket(Request $request, $ticketId, MpdfFactory $MpdfFactory): Response
{
$em = $this->entityManager;
$booking = $em->getRepository(BookingSeat::class)->findOneBy([
'id' => $ticketId
]);
$payment = null;
if ($booking->getPaidVia() == 'MPESA' || $booking->getPaidVia() == 'CASH_MPESA') {
/** @var BookingMpesa $payment */
$payment = $em->getRepository(BookingMpesa::class)->findOneBy([
'booking' => $booking
]);
}
$defaultConfig = $MpdfFactory->getDefaultConfigVariables();
$fontDirs = $defaultConfig['fontDir'];
$defaultFontConfig = $MpdfFactory->getDefaultFontVariables();
$fontData = $defaultFontConfig['fontdata'];
// dump($fontData);die;
$mPdf = $MpdfFactory->createMpdfObject([
/* 'fontDir' => array_merge($fontDirs, [
'../../public/fos/fonts/Tangerine',
'../../public/fos/fonts/OdibeeSans',
'../../public/fos/fonts/Mogra',
'../../public/fos/fonts/Libre_Baskerville',
]),
'fontdata' => $fontData + [
'tangerine' => [
'B' => 'Tangerine-Bold.ttf',
'R' => 'Tangerine-Regular.ttf'
],
'odibeesans' => [
'R' => 'OdibeeSans-Regular.ttf',
'I' => 'OdibeeSans-Regular.ttf'
],
'mogra' => [
'R' => 'Mogra-Bold.ttf',
'I' => 'Mogra-Regular.ttf',
],
'librebaskerville' => [
'R' => 'LibreBaskerville-Regular.ttf',
'B' => 'LibreBaskerville-Bold.ttf',
'I' => 'LibreBaskerville-Italic.ttf',
]
],*/
'mode' => 'utf-8',
'format' => [80, 250],
'margin_header' => 5,
'margin_footer' => 5,
'orientation' => 'P'
]);
$date = new DateTime();
$time = $date->getTimestamp();
// $mPdf->SetTopMargin("1");
// $qrCode = new QrCode('');
try {
$receiptType = 'TICKET';
/* $timsQRCode = 'ticket';
$timsqrCode = new QrCode($timsQRCode);
$output = new Png();
$data = $output->output($qrCode, 100, [255, 255, 255], [0, 0, 0]);
$timsQRCodeData = $output->output($timsqrCode, 150, [255, 255, 255], [0, 0, 0]);
file_put_contents('assets/qrcode/qrcode.png', $data);
file_put_contents('assets/qrcode/timsQRCode.png', $timsQRCodeData);*/
dump($payment);
$idBase64 = base64_encode($booking->getId());
$logoImage = file_get_contents('../public/bus/bus_1.png');
$logoBase64 = base64_encode($logoImage);
$mPdf->WriteHTML($this->renderView('bus/ticket/ticket.html.twig', [
'booking' => $booking,
'receiptType' => $receiptType,
'payment' => $payment,
'id' => $idBase64,
'logo' => $logoBase64
]));
$file = "../tickets/ticket_{$ticketId}.pdf";
// return $MpdfFactory->createDownloadResponse($mPdf, "ticket_{$ticketId}.pdf", Response::HTTP_OK, ["Set-Cookie", "fileDownload=true; path=/"]);
return $MpdfFactory->createInlineResponse($mPdf, $file);
} catch (\Exception $e) {
return new Response($e->getMessage(), Response::HTTP_BAD_REQUEST);
}
// return new Response("error occurred");
}
/**
* @Route("/daily_account", methods={"GET","POST"}, name="busDailyAccount")
*/
public function dailyAccount(Request $request): Response
{
if ($request->isMethod('POST')) {
$em = $this->entityManager;
$today = new \DateTimeImmutable();
$date = $today->format('Y-m-d');
$user_id = $this->getUser()->getId();
/** @var UserStation $userTown */
$userTown = $this->entityManager->getRepository(UserStation::class)->findOneBy([
'user' => $this->getUser(),
'isActive' => true
], ['id' => 'DESC']);
// Status filter added (ai_changes/BE-12): a bus_booking_seat row
// used to always mean "paid" - now that pending/failed online
// bookings can create one before payment succeeds, both queries
// below need to exclude those explicitly. A NULL booking_id
// still means "counter booking, always paid", so those stay in
// unconditionally via the LEFT JOIN + IS NULL check.
$sql = " SELECT d.route_name,a.paid_via,sum(a.amount) amount,c.trip_date FROM bus_booking_seat a
JOIN bus_seat b ON b.id = a.seat_id
JOIN bus_trip c ON c.id = b.trip_id
JOIN bus_route d ON d.id = c.route_id
LEFT JOIN bus_booking bk ON bk.id = a.booking_id
WHERE date(a.created_at) = cast('2026-02-06' as date)
AND (bk.id IS NULL OR bk.status = 'confirmed')
GROUP BY paid_via, d.id,b.trip_id";
$sqlTotal = "SELECT sum(a.amount) as total FROM bus_booking_seat a
LEFT JOIN bus_booking bk ON bk.id = a.booking_id
WHERE a.created_by = {$user_id} AND date(a.created_at) = cast('2026-02-06' as date)
AND (bk.id IS NULL OR bk.status = 'confirmed')";
$conn = $em->getConnection();
try {
$stmt = $conn->prepare($sql);
$amounts = $stmt->executeQuery()->fetchAllAssociative();
$stmtTotal = $conn->prepare($sqlTotal);
$total = $stmtTotal->executeQuery()->fetchAssociative();
return $this->render('bus/daily_account.html.twig', [
'date' => $date,
'amounts' => $amounts,
'total' => $total,
'user_town' => $userTown
]);
} catch (Exception $e) {
}
}
return $this->render('bus/daily_account.html.twig');
}
/**
* @Route("/daily_account/list", methods={"POST", "GET"}, name="busDailyAccountList")
*/
public function dailyAccountList(Request $request): JsonResponse
{
$em = $this->entityManager;
$page = (int)($request->request->get('page') ?? $request->query->get('page') ?? 1);
$rows = (int)($request->request->get('rows') ?? $request->query->get('rows') ?? 30);
$offset = max(0, ($page - 1) * $rows);
$searchDate = $request->request->get('date') ?? $request->query->get('date');
if (!$searchDate) {
$searchDate = (new \DateTime())->format('Y-m-d');
}
$params = ['searchDate' => $searchDate];
$sqlTotal = "SELECT COUNT(a.id) as count_total, SUM(a.amount) as sum_amount
FROM bus_booking_seat a
LEFT JOIN bus_booking bk ON bk.id = a.booking_id
WHERE date(a.created_at) = :searchDate
AND (bk.id IS NULL OR bk.status = 'confirmed')";
$sqlRows = "SELECT a.id, a.passenger, a.phone, a.amount, a.paid_via,
DATE_FORMAT(a.created_at, '%Y-%m-%d %H:%i') as created_at,
b.seat_number,
DATE_FORMAT(c.trip_date, '%Y-%m-%d') as trip_date,
d.route_name,
COALESCE(st1.station_name, 'N/A') as origin_name,
COALESCE(st2.station_name, 'N/A') as dest_name,
COALESCE(u.username, 'System') as clerk_name
FROM bus_booking_seat a
JOIN bus_seat b ON b.id = a.seat_id
JOIN bus_trip c ON c.id = b.trip_id
JOIN bus_route d ON d.id = c.route_id
LEFT JOIN bus_stop orig ON orig.id = a.origin
LEFT JOIN station st1 ON st1.id = orig.station_id
LEFT JOIN bus_stop dest ON dest.id = a.destination
LEFT JOIN station st2 ON st2.id = dest.station_id
LEFT JOIN user u ON u.id = a.created_by
LEFT JOIN bus_booking bk ON bk.id = a.booking_id
WHERE date(a.created_at) = :searchDate
AND (bk.id IS NULL OR bk.status = 'confirmed')
ORDER BY a.id DESC
LIMIT {$rows} OFFSET {$offset}";
$conn = $em->getConnection();
$totalData = $conn->fetchAssociative($sqlTotal, $params);
$totalCount = (int)($totalData['count_total'] ?? 0);
$sumAmount = (float)($totalData['sum_amount'] ?? 0);
$rowsData = $conn->fetchAllAssociative($sqlRows, $params);
return new JsonResponse([
'total' => $totalCount,
'rows' => $rowsData,
'footer' => [
[
'passenger' => 'TOTAL FARES',
'amount' => $sumAmount
]
]
]);
}
/**
* @Route("/booking/seat_details/{id}", methods={"GET", "POST"}, name="busBookingSeatDetails")
*/
public function getBookingSeatDetails($id): Response
{
$em = $this->entityManager;
$conn = $em->getConnection();
$sql = "SELECT a.id as ticket_id, a.passenger, a.phone, a.amount, a.paid_via,
DATE_FORMAT(a.created_at, '%Y-%m-%d %H:%i:%s') as created_at,
b.seat_number, b.status as seat_status,
DATE_FORMAT(c.trip_date, '%Y-%m-%d') as trip_date,
d.route_name,
COALESCE(st1.station_name, 'N/A') as origin_name,
COALESCE(st2.station_name, 'N/A') as dest_name,
COALESCE(u.username, 'System') as clerk_name,
bk.id as booking_group_id,
bk.status as booking_status,
m.transaction_id as mpesa_receipt
FROM bus_booking_seat a
JOIN bus_seat b ON b.id = a.seat_id
JOIN bus_trip c ON c.id = b.trip_id
JOIN bus_route d ON d.id = c.route_id
LEFT JOIN bus_stop orig ON orig.id = a.origin
LEFT JOIN station st1 ON st1.id = orig.station_id
LEFT JOIN bus_stop dest ON dest.id = a.destination
LEFT JOIN station st2 ON st2.id = dest.station_id
LEFT JOIN user u ON u.id = a.created_by
LEFT JOIN bus_booking bk ON bk.id = a.booking_id
LEFT JOIN bus_booking_mpesa bbm ON bbm.bus_booking_id = bk.id
LEFT JOIN mpesa m ON m.id = bbm.mpesa_id
WHERE a.id = :id
LIMIT 1";
$details = $conn->fetchAssociative($sql, ['id' => $id]);
if (!$details) {
return new Response('<div class="alert alert-warning p-2">Details not found for ticket #' . htmlspecialchars((string)$id) . '</div>');
}
return $this->render('bus/seat_detail_panel.html.twig', [
'd' => $details
]);
}
private function addTrip(\App\Entity\Bus\Route $route, string $date)
{
$em = $this->entityManager;
$dateTime = DateTime::createFromFormat('Y-m-d', $date);
if (!$dateTime) {
$dateTime = new DateTime($date);
}
/** @var RouteFareRule $fareRule */
$fareRule = $em->getRepository(RouteFareRule::class)->findOneBy([
'route' => $route,
]);
$trip = new Trip();
$trip->setCreatedBy($this->getUser());
$trip->setCreatedAt(new DateTime());
$trip->setTripDate($dateTime);
$trip->setRoute($route);
$trip->setDestination($route->getDestination());
$trip->setOrigin($route->getOrigin());
$trip->setTripIndex(1);
if ($fareRule) {
$trip->setFare($fareRule->getNormalFare());
}
$departureStored = $route->getDepartureTime();
$arrivalStored = $route->getEta();
$baseDate = clone $dateTime;
$departure = (clone $baseDate)->setTime(
(int)$departureStored->format('H'),
(int)$departureStored->format('i'),
(int)$departureStored->format('s')
);
$arrival = (clone $baseDate)->setTime(
(int)$arrivalStored->format('H'),
(int)$arrivalStored->format('i'),
(int)$arrivalStored->format('s')
);
if ($arrival <= $departure) {
$arrival->modify('+1 day');
}
$trip->setDepartureTime($departure);
$trip->setEta($arrival);
/** @var Vehicle $vehicle */
$vehicle = $em->getRepository(Vehicle::class)->findOneBy([
'regNumber' => 'KCE990Q'
]);
$seats = [];
for ($i = 1; $i <= $vehicle->getPassengerCapacity(); $i++) {
$seat = new Seat();
$seat->setStatus('AVAILABLE');
$seat->setSeatNumber($i);
$seat->setTrip($trip);
$seat->setIsBooked(false);
$seat->setCreatedAt(new DateTime());
$seats[] = $seat;
}
$trip->setSeats($seats);
$trip->setVehicle($vehicle);
$tripVehicle = new TripVehicle();
$tripVehicle->setCreatedAt(new DateTime());
$tripVehicle->setTrip($trip);
$tripVehicle->setCreatedBy($this->getUser());
$tripVehicle->setIsCancelled(false);
$tripVehicle->setVehicle($vehicle);
$this->entityManager->persist($tripVehicle);
$this->entityManager->persist($trip);
$this->entityManager->flush();
}
private
function jsonTestData()
{
$jsonData = '[{
"id": 741421,
"account_date": "2023-10-09",
"expenses": 0,
"created_at": "2023-10-09 19:59:54",
"station_name": "MOMBASA",
"sender_name": "patrick kinyua",
"sender_phone_number": "0727 044 524",
"receiver_name": "luke njoroge",
"receiver_phone_number": "0705 179 108",
"amount": 2100,
"is_cancelled": 0
},
{
"id": 741405,
"account_date": "2023-10-09",
"expenses": 0,
"created_at": "2023-10-09 19:19:03",
"station_name": "MERU",
"sender_name": "phineas kirimi",
"sender_phone_number": "0791 713 924",
"receiver_name": "patrick munoru",
"receiver_phone_number": "0720 949 054",
"amount": 200,
"is_cancelled": 0
},
{
"id": 741395,
"account_date": "2023-10-09",
"expenses": 0,
"created_at": "2023-10-09 19:09:05",
"station_name": "kitui",
"sender_name": "scout shop",
"sender_phone_number": "0712 907 877",
"receiver_name": "winfred ndolo",
"receiver_phone_number": "0724 528 806",
"amount": 200,
"is_cancelled": 0
},
{
"id": 741393,
"account_date": "2023-10-09",
"expenses": 0,
"created_at": "2023-10-09 19:07:04",
"station_name": "SIAKAGO",
"sender_name": "james gichuki",
"sender_phone_number": "0722 352 992",
"receiver_name": "richard waitherero",
"receiver_phone_number": "0728 454 129",
"amount": 300,
"is_cancelled": 0
},
{
"id": 741378,
"account_date": "2023-10-09",
"expenses": 0,
"created_at": "2023-10-09 18:55:25",
"station_name": "NAIROBI",
"sender_name": "denis munene",
"sender_phone_number": "0740 270 344",
"receiver_name": "tom ochieng",
"receiver_phone_number": "0715 553 018",
"amount": 200,
"is_cancelled": 0
},
{
"id": 741370,
"account_date": "2023-10-09",
"expenses": 0,
"created_at": "2023-10-09 18:43:13",
"station_name": "NAIROBI",
"sender_name": "peter njagi",
"sender_phone_number": "0722 883 774",
"receiver_name": "jackson kanambiu",
"receiver_phone_number": "0714 203 693",
"amount": 200,
"is_cancelled": 0
},
{
"id": 741355,
"account_date": "2023-10-09",
"expenses": 0,
"created_at": "2023-10-09 18:32:02",
"station_name": "MOMBASA",
"sender_name": "mary mbugi",
"sender_phone_number": "0727 897 097",
"receiver_name": "elizabeth nyawira",
"receiver_phone_number": "0706 221 351",
"amount": 300,
"is_cancelled": 0
},
{
"id": 741351,
"account_date": "2023-10-09",
"expenses": 0,
"created_at": "2023-10-09 18:25:01",
"station_name": "NAIROBI",
"sender_name": "david thairu",
"sender_phone_number": "0722 280 846",
"receiver_name": "daniel kimondo",
"receiver_phone_number": "0724 215 522",
"amount": 200,
"is_cancelled": 0
},
{
"id": 741330,
"account_date": "2023-10-09",
"expenses": 0,
"created_at": "2023-10-09 17:58:08",
"station_name": "NAIROBI",
"sender_name": "karen karimi",
"sender_phone_number": "0714 509 299",
"receiver_name": "victor muthoni",
"receiver_phone_number": "0710 329 231",
"amount": 200,
"is_cancelled": 0
},
{
"id": 741328,
"account_date": "2023-10-09",
"expenses": 0,
"created_at": "2023-10-09 17:56:42",
"station_name": "KATHWANA",
"sender_name": "samwel",
"sender_phone_number": "0722 595 844",
"receiver_name": "dirangu ndungu",
"receiver_phone_number": "0721 207 371",
"amount": 250,
"is_cancelled": 0
},
{
"id": 741301,
"account_date": "2023-10-09",
"expenses": 0,
"created_at": "2023-10-09 17:35:27",
"station_name": "MOMBASA",
"sender_name": "edith kimani",
"sender_phone_number": "0714 567 205",
"receiver_name": "okumu",
"receiver_phone_number": "0714 567 205",
"amount": 300,
"is_cancelled": 0
},
{
"id": 741289,
"account_date": "2023-10-09",
"expenses": 0,
"created_at": "2023-10-09 17:26:09",
"station_name": "MTWAPA",
"sender_name": "caroline waweru",
"sender_phone_number": "0712 353 926",
"receiver_name": "GLADYS WAWERU",
"receiver_phone_number": "0723 950 679",
"amount": 300,
"is_cancelled": 0
},
{
"id": 741270,
"account_date": "2023-10-09",
"expenses": 0,
"created_at": "2023-10-09 17:03:24",
"station_name": "MWEA",
"sender_name": "FOCUS EMBU",
"sender_phone_number": "0716 447 206",
"receiver_name": "FOCUS MWEA",
"receiver_phone_number": "0721 606 746",
"amount": 150,
"is_cancelled": 0
},
{
"id": 741266,
"account_date": "2023-10-09",
"expenses": 0,
"created_at": "2023-10-09 16:58:43",
"station_name": "THIKA",
"sender_name": "KIMATHI KELVIN",
"sender_phone_number": "0726 116 760",
"receiver_name": "JAMES NGINGI",
"receiver_phone_number": "0728 171 491",
"amount": 200,
"is_cancelled": 0
},
{
"id": 741261,
"account_date": "2023-10-09",
"expenses": 0,
"created_at": "2023-10-09 16:54:46",
"station_name": "NAIROBI",
"sender_name": "ROSALINE WAWIRA",
"sender_phone_number": "0728 288 950",
"receiver_name": "eras karimi",
"receiver_phone_number": "0724 867 838",
"amount": 200,
"is_cancelled": 0
},
{
"id": 741248,
"account_date": "2023-10-09",
"expenses": 0,
"created_at": "2023-10-09 16:47:18",
"station_name": "MOMBASA",
"sender_name": "jemmimah",
"sender_phone_number": "0711 485 954",
"receiver_name": "rose khaidi",
"receiver_phone_number": "0706 749 991",
"amount": 300,
"is_cancelled": 0
},
{
"id": 741209,
"account_date": "2023-10-09",
"expenses": 0,
"created_at": "2023-10-09 16:05:38",
"station_name": "kitui",
"sender_name": "eunice mambiro",
"sender_phone_number": "0720 997 508",
"receiver_name": "samwel muturi",
"receiver_phone_number": "0715 143 362",
"amount": 200,
"is_cancelled": 0
},
{
"id": 741185,
"account_date": "2023-10-09",
"expenses": 0,
"created_at": "2023-10-09 15:49:11",
"station_name": "NAIROBI",
"sender_name": "johnson njeru",
"sender_phone_number": "0724 586 321",
"receiver_name": "jane njoki njeru",
"receiver_phone_number": "0722 398 853",
"amount": 300,
"is_cancelled": 0
},
{
"id": 741175,
"account_date": "2023-10-09",
"expenses": 0,
"created_at": "2023-10-09 15:36:39",
"station_name": "NAIROBI",
"sender_name": "alexande david",
"sender_phone_number": "0795 762 309",
"receiver_name": "peter aunga",
"receiver_phone_number": "0715 044 187",
"amount": 200,
"is_cancelled": 0
},
{
"id": 741172,
"account_date": "2023-10-09",
"expenses": 0,
"created_at": "2023-10-09 15:33:17",
"station_name": "NAIROBI",
"sender_name": "j kimondo",
"sender_phone_number": "0722 830 014",
"receiver_name": "fred muiruri",
"receiver_phone_number": "0705 945 561",
"amount": 200,
"is_cancelled": 0
}
]';
$arrayedFromJson = json_decode($jsonData, true);
return $arrayedFromJson;
}
/**
* @Route("/stk-push/initiate", methods={"POST"}, name="bus_stk_push_initiate")
*/
public function initiateStkPush(Request $request): JsonResponse
{
$em = $this->entityManager;
$phone = $request->request->get('phone');
$amount = (float) $request->request->get('amount');
$seatNumber = $request->request->get('seat');
$tripId = $request->request->get('tripId');
if (!$phone || !$amount) {
return new JsonResponse(['success' => false, 'message' => 'Phone and amount are required.'], Response::HTTP_BAD_REQUEST);
}
$phone = preg_replace('/[^0-9]/', '', (string)$phone);
if (strpos($phone, '0') === 0) {
$phone = '254' . substr($phone, 1);
} elseif (strpos($phone, '7') === 0 || strpos($phone, '1') === 0) {
$phone = '254' . $phone;
}
/** @var MpesaAuth $mpesaAuth */
$mpesaAuth = $em->getRepository(MpesaAuth::class)->findOneBy([
'paybill' => '4005975'
]);
if (!$mpesaAuth) {
$mpesaAuth = $em->getRepository(MpesaAuth::class)->findOneBy([
'shortCodeType' => 'BUS_STK'
]);
}
if (!$mpesaAuth) {
return new JsonResponse(['success' => false, 'message' => 'M-Pesa STK configuration not found.'], Response::HTTP_INTERNAL_SERVER_ERROR);
}
$token = $this->getMpesaStkToken($mpesaAuth);
if (!$token) {
return new JsonResponse(['success' => false, 'message' => 'Failed to generate M-Pesa OAuth Access Token.'], Response::HTTP_INTERNAL_SERVER_ERROR);
}
$timestamp = (new DateTime())->format('YmdHis');
$businessShortCode = $mpesaAuth->getPaybill();
$passKey = $mpesaAuth->getPassKey();
$password = base64_encode($businessShortCode . $passKey . $timestamp);
$callbackUrl = $this->generateUrl('bus_stk_push_callback', [], UrlGeneratorInterface::ABSOLUTE_URL);
$postData = [
'BusinessShortCode' => $businessShortCode,
'Password' => $password,
'Timestamp' => $timestamp,
'TransactionType' => 'CustomerPayBillOnline',
'Amount' => (int) $amount,
'PartyA' => $phone,
'PartyB' => $businessShortCode,
'PhoneNumber' => $phone,
'CallBackURL' => $callbackUrl,
'AccountReference' => 'SEAT' . ($seatNumber ? '_' . $seatNumber : ''),
'TransactionDesc' => 'Bus Booking Seat ' . $seatNumber
];
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://api.safaricom.co.ke/mpesa/stkpush/v1/processrequest",
CURLOPT_HTTPHEADER => [
"Content-Type: application/json",
"Authorization: Bearer {$token}"
],
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => json_encode($postData),
CURLOPT_SSL_VERIFYPEER => false,
CURLOPT_TIMEOUT => 30,
]);
$responseRaw = curl_exec($curl);
curl_close($curl);
$res = json_decode($responseRaw, true);
if (isset($res['ResponseCode']) && $res['ResponseCode'] == '0') {
$checkoutRequestId = $res['CheckoutRequestID'] ?? '';
$merchantRequestId = $res['MerchantRequestID'] ?? '';
$mpesaReq = new MpesaPaymentRequest();
$mpesaReq->setMerchantRequestId($merchantRequestId);
$mpesaReq->setCheckoutRequestId($checkoutRequestId);
$mpesaReq->setDescription("Bus Booking Seat {$seatNumber} Trip {$tripId}");
$mpesaReq->setMessage($res['CustomerMessage'] ?? $res['ResponseDescription'] ?? '');
$mpesaReq->setPhone((int) $phone);
$mpesaReq->setCreatedAt(new DateTime());
$em->persist($mpesaReq);
$em->flush();
return new JsonResponse([
'success' => true,
'checkoutRequestId' => $checkoutRequestId,
'message' => 'STK Push sent to ' . $phone . '. Please enter PIN on phone.'
]);
} else {
$errorMsg = $res['errorMessage'] ?? $res['ResponseDescription'] ?? 'Failed to send STK Push prompt.';
return new JsonResponse([
'success' => false,
'message' => $errorMsg
], Response::HTTP_BAD_REQUEST);
}
}
/**
* @Route("/stk-push/callback", methods={"POST"}, name="bus_stk_push_callback")
*/
public function callbackStkPush(Request $request): JsonResponse
{
$em = $this->entityManager;
$content = $request->getContent();
$data = json_decode($content, true);
$stkCallback = $data['Body']['stkCallback'] ?? null;
if ($stkCallback) {
$merchantRequestId = $stkCallback['MerchantRequestID'] ?? '';
$checkoutRequestId = $stkCallback['CheckoutRequestID'] ?? '';
$resultCode = (int) ($stkCallback['ResultCode'] ?? -1);
$resultDesc = $stkCallback['ResultDesc'] ?? '';
$mpesaResp = new MpesaResponse();
$mpesaResp->setMerchantRequestId($merchantRequestId);
$mpesaResp->setCheckoutRequestId($checkoutRequestId);
$mpesaResp->setCode($resultCode);
$mpesaResp->setDescription($resultDesc);
$mpesaResp->setCreatedAt(new DateTime());
$em->persist($mpesaResp);
if ($resultCode === 0) {
$items = $stkCallback['CallbackMetadata']['Item'] ?? [];
$receipt = '';
$amount = 0;
$phone = '';
foreach ($items as $item) {
if (($item['Name'] ?? '') === 'MpesaReceiptNumber') {
$receipt = (string) $item['Value'];
}
if (($item['Name'] ?? '') === 'Amount') {
$amount = (float) $item['Value'];
}
if (($item['Name'] ?? '') === 'PhoneNumber') {
$phone = (string) $item['Value'];
}
}
if ($receipt) {
$existing = $em->getRepository(Mpesa::class)->findOneBy(['transactionId' => $receipt]);
if (!$existing) {
$mpesa = new Mpesa();
$mpesa->setTransactionId($receipt);
$mpesa->setTransactionType('CustomerPayBillOnline');
$mpesa->setTransactionTime(date('Y-m-d H:i:s'));
$mpesa->setTransactionAmount($amount);
$mpesa->setRefNumber($checkoutRequestId);
$mpesa->setShortCode(4005975);
$mpesa->setBalance(0);
$mpesa->setMsisdn($phone);
$mpesa->setFirstName('M-Pesa');
$mpesa->setLastName('Customer');
$mpesa->setMiddleName('');
$mpesa->setCreatedAt(new DateTime());
$mpesa->setIsUsed(false);
$em->persist($mpesa);
}
}
}
$em->flush();
}
return new JsonResponse(['ResultCode' => 0, 'ResultDesc' => 'Accepted']);
}
/**
* @Route("/stk-push/status/{checkoutRequestId}", methods={"GET"}, name="bus_stk_push_status")
*/
public function checkStkPushStatus(string $checkoutRequestId): JsonResponse
{
$em = $this->entityManager;
$response = $em->getRepository(MpesaResponse::class)->findOneBy([
'checkoutRequestId' => $checkoutRequestId
]);
if ($response) {
if ($response->getCode() === 0) {
$mpesa = $em->getRepository(Mpesa::class)->findOneBy([
'refNumber' => $checkoutRequestId
]);
if (!$mpesa) {
$mpesa = $em->getRepository(Mpesa::class)->findOneBy([
'shortCode' => 4005975,
'isUsed' => false
], ['id' => 'DESC']);
}
return new JsonResponse([
'status' => 'SUCCESS',
'mpesaId' => $mpesa ? $mpesa->getId() : null,
'receipt' => $mpesa ? $mpesa->getTransactionId() : '',
'amount' => $mpesa ? $mpesa->getTransactionAmount() : 0,
'message' => 'Payment received successfully!'
]);
} else {
return new JsonResponse([
'status' => 'FAILED',
'message' => $response->getDescription() ?: 'Payment failed or cancelled by customer.'
]);
}
}
return new JsonResponse(['status' => 'PENDING']);
}
private function getMpesaStkToken(MpesaAuth $mpesaAuth): ?string
{
if ($mpesaAuth->getToken() && $mpesaAuth->getTokenUpdatedAt()) {
$diff = (new DateTime())->getTimestamp() - $mpesaAuth->getTokenUpdatedAt()->getTimestamp();
if ($diff < 3000) {
return $mpesaAuth->getToken();
}
}
$url = "https://api.safaricom.co.ke/oauth/v1/generate?grant_type=client_credentials";
$credentials = base64_encode("{$mpesaAuth->getConsumerKey()}:{$mpesaAuth->getConsumerSecret()}");
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => $url,
CURLOPT_HTTPHEADER => [
"Content-Type: application/json",
"Authorization: Basic {$credentials}"
],
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPGET => true,
CURLOPT_SSL_VERIFYPEER => false,
CURLOPT_TIMEOUT => 15,
]);
$response = curl_exec($curl);
curl_close($curl);
if ($response) {
$result = json_decode($response, true);
if (isset($result['access_token'])) {
$token = $result['access_token'];
$mpesaAuth->setToken($token);
$mpesaAuth->setTokenUpdatedAt(new DateTime());
$this->entityManager->flush();
return $token;
}
}
return $mpesaAuth->getToken();
}
/**
* @Route("/trip/{trip}/fare", methods={"GET"}, name="getTripFare")
*/
public function getTripFare(Request $request, Trip $trip): JsonResponse
{
$em = $this->entityManager;
$originId = $request->query->get('origin');
$destinationId = $request->query->get('destination');
$originStop = $originId ? $em->getRepository(Stop::class)->find($originId) : null;
$destStop = $destinationId ? $em->getRepository(Stop::class)->find($destinationId) : null;
$fare = $this->getDatabaseFare($trip, $originStop, $destStop);
return new JsonResponse(['fare' => $fare]);
}
private function getDatabaseFare(Trip $trip, ?Stop $originStop = null, ?Stop $destinationStop = null): float
{
$em = $this->entityManager;
$originStation = $originStop ? $originStop->getStation() : null;
$destStation = $destinationStop ? $destinationStop->getStation() : null;
if ($originStation && $destStation) {
$fareRule = $em->getRepository(RouteFareRule::class)->findOneBy([
'route' => $trip->getRoute(),
'origin' => $originStation,
'destination' => $destStation
]);
if ($fareRule && $fareRule->getNormalFare() > 0) {
return (float) $fareRule->getNormalFare();
}
}
$fareRule = $em->getRepository(RouteFareRule::class)->findOneBy([
'route' => $trip->getRoute()
]);
if ($fareRule && $fareRule->getNormalFare() > 0) {
return (float) $fareRule->getNormalFare();
}
return (float) ($trip->getFare() ?: 0);
}
}