<?php
namespace CompanyGroupBundle\Controller;
use ApplicationBundle\Constants\BuddybeeConstant;
use ApplicationBundle\Controller\GenericController;
use ApplicationBundle\Modules\Authentication\Constants\UserConstants;
use ApplicationBundle\Modules\Buddybee\Buddybee;
use CompanyGroupBundle\Entity\CompanyGroup;
use CompanyGroupBundle\Entity\EntityApplicantDetails;
use CompanyGroupBundle\Entity\EntityInvoice;
use CompanyGroupBundle\Entity\SubscriptionQuote;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
/**
* Admin invoice management.
* Entity manager: company_group
*/
class AdminInvoiceController extends GenericController
{
const INVOICES_PER_PAGE = 20;
private function requireAdminAccess(Request $request): bool
{
$session = $request->getSession();
$userId = (int)$session->get(UserConstants::USER_ID, 0);
$isBuddybee = (int)$session->get(UserConstants::IS_BUDDYBEE_ADMIN, 0);
$allAccess = (int)$session->get(UserConstants::ALL_MODULE_ACCESS_FLAG, 0);
return $userId > 0 && ($isBuddybee === 1 || $allAccess === 1);
}
private function invoiceStatus(EntityInvoice $invoice): string
{
if ((int)$invoice->getStage() === BuddybeeConstant::ENTITY_INVOICE_STAGE_COMPLETED || (int)$invoice->getIsProcessed() === 1) {
return 'paid';
}
if ((int)$invoice->getStage() === BuddybeeConstant::ENTITY_INVOICE_STAGE_FAILED) {
return 'failed';
}
if ((int)$invoice->getStage() === BuddybeeConstant::ENTITY_INVOICE_STAGE_CANCELLED) {
return 'cancelled';
}
return 'pending';
}
private function normalizeLegacyInvoice(EntityManagerInterface $em, EntityInvoice $invoice): array
{
$successActionData = json_decode((string)$invoice->getSuccessActionData(), true);
if (!is_array($successActionData)) {
$successActionData = [];
}
$quote = null;
$quoteId = (int)($successActionData['quoteId'] ?? 0);
if ($quoteId > 0) {
$quote = $em->getRepository(SubscriptionQuote::class)->find($quoteId);
}
$appId = (int)$invoice->getAppId();
$company = $appId > 0 ? $em->getRepository(CompanyGroup::class)->findOneBy(['appId' => $appId]) : null;
$billTo = $invoice->getBillToId() ? $em->getRepository(EntityApplicantDetails::class)->find((int)$invoice->getBillToId()) : null;
$companyName = '';
if ($company && method_exists($company, 'getCompanyName')) {
$companyName = (string)$company->getCompanyName();
}
if ($companyName === '' && $quote) {
$companyName = (string)$quote->getCompanyName();
}
if ($companyName === '' && isset($successActionData['pendingCompany']['companyName'])) {
$companyName = (string)$successActionData['pendingCompany']['companyName'];
}
$customerName = $quote ? (string)$quote->getCustomerName() : '';
if ($customerName === '' && $billTo) {
$customerName = trim((string)$billTo->getFirstname() . ' ' . (string)$billTo->getLastName());
}
if ($customerName === '' && isset($successActionData['pendingCompany']['customerName'])) {
$customerName = (string)$successActionData['pendingCompany']['customerName'];
}
$customerEmail = $quote ? (string)$quote->getCustomerEmail() : '';
if ($customerEmail === '' && isset($successActionData['pendingCompany']['customerEmail'])) {
$customerEmail = (string)$successActionData['pendingCompany']['customerEmail'];
}
if ($customerEmail === '' && $billTo) {
$customerEmail = (string)($billTo->getOAuthEmail() ?: $billTo->getEmail());
}
$metaQuote = [
'id' => $quote ? (int)$quote->getId() : 0,
'createdAt' => $quote ? $quote->getCreatedAt() : null,
'status' => $quote ? (string)$quote->getStatus() : '',
];
$invoiceDate = $invoice->getInvoiceDate();
$expiryTs = (int)$invoice->getExpireIfUnpaidTs();
$dueAt = $expiryTs > 0 ? (new \DateTime())->setTimestamp($expiryTs) : null;
$status = $this->invoiceStatus($invoice);
$manualReference = isset($successActionData['manualTransactionReference']) ? (string)$successActionData['manualTransactionReference'] : '';
return [
'id' => (int)$invoice->getId(),
'invoiceNumber' => (string)($invoice->getDocumentHash() ?: ('EI-' . str_pad((string)$invoice->getId(), 8, '0', STR_PAD_LEFT))),
'companyName' => $companyName,
'customerName' => $customerName,
'customerEmail' => $customerEmail,
'planType' => (string)($successActionData['planType'] ?? ($quote ? $quote->getPlanType() : '')),
'billingCycle' => (string)($successActionData['billingCycle'] ?? ($quote ? $quote->getBillingCycle() : 'monthly')),
'paymentType' => $invoice->getAmountTransferGateWayHash() === 'stripe' ? 'automatic' : 'manual',
'finalAmount' => (float)$invoice->getAmount(),
'amount' => (float)$invoice->getAmount(),
'discountAmount' => (float)$invoice->getPromoDiscountAmount(),
'status' => $status,
'createdAt' => $invoiceDate,
'dueAt' => $dueAt,
'paidAt' => $status === 'paid' ? $invoiceDate : null,
'gatewayTransactionId' => $manualReference,
'normalUserCount' => (int)($successActionData['userModification'] ?? 0),
'adminUserCount' => (int)($successActionData['adminModification'] ?? 0),
'mlUserCount' => (int)($successActionData['mlUserCount'] ?? 0),
'notes' => isset($successActionData['reason']) ? ('Reason: ' . (string)$successActionData['reason']) : '',
'quote' => $metaQuote,
'legacyInvoice' => $invoice,
];
}
// =========================================================================
// LIST INVOICES
// =========================================================================
/**
* GET /admin/invoices
*/
public function ListInvoicesAction(Request $request)
{
if (!$this->requireAdminAccess($request)) {
return $this->redirectToRoute('dashboard');
}
$em = $this->getDoctrine()->getManager('company_group');
$page = max(1, (int)$request->query->get('page', 1));
$offset = ($page - 1) * self::INVOICES_PER_PAGE;
$filters = [
'status' => $request->query->get('status', ''),
'payment_type' => $request->query->get('payment_type', ''),
'search' => trim($request->query->get('q', '')),
];
$qb = $em->getRepository(EntityInvoice::class)->createQueryBuilder('i')
->where('i.invoiceType = :invoiceType')
->setParameter('invoiceType', BuddybeeConstant::ENTITY_INVOICE_TYPE_PAYMENT_TO_HONEYBEE)
->orderBy('i.Id', 'DESC');
if ($filters['payment_type'] === 'automatic') {
$qb->andWhere('i.amountTransferGateWayHash = :gatewayAutomatic')
->setParameter('gatewayAutomatic', 'stripe');
} elseif ($filters['payment_type'] === 'manual') {
$qb->andWhere('i.amountTransferGateWayHash != :gatewayAutomatic')
->setParameter('gatewayAutomatic', 'stripe');
}
if ($filters['status'] === 'paid') {
$qb->andWhere('i.stage = :stagePaid OR i.isProcessed = 1')
->setParameter('stagePaid', BuddybeeConstant::ENTITY_INVOICE_STAGE_COMPLETED);
} elseif ($filters['status'] === 'pending') {
$qb->andWhere('i.stage = :stagePending AND i.isProcessed = 0')
->setParameter('stagePending', BuddybeeConstant::ENTITY_INVOICE_STAGE_INITIATED);
} elseif ($filters['status'] === 'failed') {
$qb->andWhere('i.stage = :stageFailed')
->setParameter('stageFailed', BuddybeeConstant::ENTITY_INVOICE_STAGE_FAILED);
} elseif ($filters['status'] === 'cancelled') {
$qb->andWhere('i.stage = :stageCancelled')
->setParameter('stageCancelled', BuddybeeConstant::ENTITY_INVOICE_STAGE_CANCELLED);
}
if ($filters['search'] !== '') {
$qb->andWhere('i.documentHash LIKE :search OR i.successActionData LIKE :search')
->setParameter('search', '%' . $filters['search'] . '%');
}
$allInvoices = $qb->getQuery()->getResult();
$normalized = array_map(function (EntityInvoice $invoice) use ($em) {
return $this->normalizeLegacyInvoice($em, $invoice);
}, $allInvoices);
if ($filters['search'] !== '') {
$needle = mb_strtolower($filters['search']);
$normalized = array_values(array_filter($normalized, function (array $invoice) use ($needle) {
$haystacks = [
(string)$invoice['invoiceNumber'],
(string)$invoice['companyName'],
(string)$invoice['customerName'],
(string)$invoice['customerEmail'],
];
foreach ($haystacks as $haystack) {
if ($haystack !== '' && mb_strpos(mb_strtolower($haystack), $needle) !== false) {
return true;
}
}
return false;
}));
}
$total = count($normalized);
$items = array_slice($normalized, $offset, self::INVOICES_PER_PAGE);
$totalPages = (int)ceil($total / self::INVOICES_PER_PAGE);
$stats = [
'total' => count(array_filter($normalized, function () { return true; })),
'pending' => count(array_filter($normalized, function (array $invoice) { return $invoice['status'] === 'pending'; })),
'paid' => count(array_filter($normalized, function (array $invoice) { return $invoice['status'] === 'paid'; })),
'failed' => count(array_filter($normalized, function (array $invoice) { return $invoice['status'] === 'failed'; })),
'revenue' => array_reduce($normalized, function ($carry, array $invoice) {
return $invoice['status'] === 'paid' ? $carry + (float)$invoice['finalAmount'] : $carry;
}, 0.0),
];
return $this->render('@CompanyGroup/pages/admin/invoices/list_invoices.html.twig', [
'page_title' => 'Invoices',
'invoices' => $items,
'total' => $total,
'currentPage' => $page,
'totalPages' => $totalPages,
'filters' => $filters,
'stats' => $stats,
]);
}
// =========================================================================
// VIEW INVOICE
// =========================================================================
/**
* GET /admin/invoices/{id}
*/
public function ViewInvoiceAction(Request $request, $id)
{
if (!$this->requireAdminAccess($request)) {
return $this->redirectToRoute('dashboard');
}
$em = $this->getDoctrine()->getManager('company_group');
$legacyInvoice = $em->getRepository(EntityInvoice::class)->find((int)$id);
if (!$legacyInvoice || (int)$legacyInvoice->getInvoiceType() !== BuddybeeConstant::ENTITY_INVOICE_TYPE_PAYMENT_TO_HONEYBEE) {
throw $this->createNotFoundException('Invoice #' . $id . ' not found.');
}
$invoice = $this->normalizeLegacyInvoice($em, $legacyInvoice);
$quote = !empty($invoice['quote']['id'])
? $em->getRepository(SubscriptionQuote::class)->find((int)$invoice['quote']['id'])
: null;
return $this->render('@CompanyGroup/pages/admin/invoices/view_invoice.html.twig', [
'page_title' => 'Invoice ' . $invoice['invoiceNumber'],
'invoice' => $invoice,
'quote' => $quote,
]);
}
// =========================================================================
// MARK PAID (manual payment)
// =========================================================================
/**
* POST /admin/invoices/{id}/mark-paid
*/
public function MarkPaidAction(Request $request, $id)
{
if (!$this->requireAdminAccess($request)) {
return new JsonResponse(['success' => false, 'message' => 'Unauthorized'], 403);
}
$em = $this->getDoctrine()->getManager('company_group');
$invoice = $em->getRepository(EntityInvoice::class)->find((int)$id);
if (!$invoice || (int)$invoice->getInvoiceType() !== BuddybeeConstant::ENTITY_INVOICE_TYPE_PAYMENT_TO_HONEYBEE) {
return new JsonResponse(['success' => false, 'message' => 'Invoice not found'], 404);
}
$txRef = trim($request->request->get('transaction_reference', ''));
if ($txRef !== '') {
$successActionData = json_decode((string)$invoice->getSuccessActionData(), true);
if (!is_array($successActionData)) {
$successActionData = [];
}
$successActionData['manualTransactionReference'] = $txRef;
$invoice->setSuccessActionData(json_encode($successActionData));
$em->flush();
}
$this->get('app.quote_company_provisioning_service')
->ensureCompanyForInvoice($invoice, $request->getSession(), $invoice->getStripeCustomerId() ?: null);
Buddybee::ProcessEntityInvoice(
$em,
(int)$invoice->getId(),
['stage' => BuddybeeConstant::ENTITY_INVOICE_STAGE_COMPLETED],
$this->container->getParameter('kernel.root_dir'),
false,
$this->container->getParameter('notification_enabled'),
$this->container->getParameter('notification_server')
);
$this->get('app.subscription_state_sync_service')->syncFromLegacyInvoice($invoice);
$successActionData = json_decode((string)$invoice->getSuccessActionData(), true);
if (!is_array($successActionData)) {
$successActionData = [];
}
$ownerId = (int)($successActionData['ownerId'] ?? $invoice->getApplicantId());
$appId = (int)($invoice->getAppId() ?: ($successActionData['appId'] ?? 0));
if ($ownerId > 0 && $appId > 0) {
$this->get('app.post_payment_company_setup_service')
->finalizeOwnerServerSync($ownerId);
}
$displayNumber = $invoice->getDocumentHash() ?: ('EI-' . str_pad((string)$invoice->getId(), 8, '0', STR_PAD_LEFT));
$this->addFlash('success', 'Invoice ' . $displayNumber . ' marked as paid.');
return $this->redirectToRoute('admin_invoice_view', ['id' => $id]);
}
// =========================================================================
// MARK FAILED
// =========================================================================
/**
* POST /admin/invoices/{id}/mark-failed
*/
public function MarkFailedAction(Request $request, $id)
{
if (!$this->requireAdminAccess($request)) {
return new JsonResponse(['success' => false, 'message' => 'Unauthorized'], 403);
}
$em = $this->getDoctrine()->getManager('company_group');
$invoice = $em->getRepository(EntityInvoice::class)->find((int)$id);
if (!$invoice || (int)$invoice->getInvoiceType() !== BuddybeeConstant::ENTITY_INVOICE_TYPE_PAYMENT_TO_HONEYBEE) {
return new JsonResponse(['success' => false, 'message' => 'Invoice not found'], 404);
}
$invoice->setStage(BuddybeeConstant::ENTITY_INVOICE_STAGE_FAILED);
$invoice->setIsProcessed(0);
$em->flush();
$displayNumber = $invoice->getDocumentHash() ?: ('EI-' . str_pad((string)$invoice->getId(), 8, '0', STR_PAD_LEFT));
$this->addFlash('info', 'Invoice ' . $displayNumber . ' marked as failed.');
return $this->redirectToRoute('admin_invoice_view', ['id' => $id]);
}
}