<?php
namespace App\Controller\Api\v3;
use App\Controller\Api\Service\ExpenseService;
use App\Controller\Api\Service\InvoiceService;
use App\Controller\Api\Service\MaterialService;
use App\Controller\Api\Service\ReferenceService;
use App\Controller\Api\Service\ProjectOrderService;
use App\Controller\Api\Service\SupplierService;
use App\Entity\Invoices;
use App\Entity\Locations;
use App\Entity\ProductCatalog;
use App\Entity\ProjectOrderExpensePositions;
use App\Entity\ProjectOrderExpenses;
use App\Entity\StockItems;
use App\Entity\Unit;
use App\Service\ExpensePdfService;
use Doctrine\ORM\EntityManagerInterface;
use phpDocumentor\Reflection\Types\This;
use Symfony\Component\Mailer\MailerInterface;
use Symfony\Component\Mime\Address;
use Symfony\Component\Mime\Email;
use Symfony\Component\Serializer\SerializerInterface;
use App\Enum\SeverityInterface;
use App\Service\SerializeService\ExpenseSerialize;
use App\Service\SerializeService\InvoiceSerialize;
use App\Service\SerializeService\ReferenceSerialize;
use App\Service\SerializeService\ProjectOrderSerialize;
use App\Service\SerializeService\SupplierSerialize;
use App\Service\StreamingService;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\StreamedResponse;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Contracts\Translation\TranslatorInterface;
use function Symfony\Component\DependencyInjection\Loader\Configurator\param;
/**
* @Route("/api/v3/expense")
*/
class ExpenseController extends AbstractController
{
/**
* STREAMED POSITION
* @Route("/stream-expense-dependencies", name="api_v3_expense_stream_expense_dependencies")
* @throws \Exception
*/
public function streamExpenseDependencies(
SupplierService $supplierService,
SupplierSerialize $supplierSerialize,
ProjectOrderService $projectOrderService,
ProjectOrderSerialize $projectOrderSerialize,
MaterialService $materialService,
ReferenceSerialize $referenceSerialize,
TranslatorInterface $translator,
EntityManagerInterface $manager
): StreamedResponse {
$streamed = new StreamingService([
"localText" => [
"completed" => $translator->trans("Ready...")
]
]);
$streamed->appendStream(
'suppliers',
$translator->trans("Loading suppliers..."),
fn() => $supplierSerialize->setEntity($supplierService->fetchAll())->serializeWithFrameworkItems()
);
$streamed->appendStream(
'materials',
$translator->trans("Loading suppliers..."),
fn() => $referenceSerialize->setEntity($materialService->fetchAll())->serializeCustomCore("material")
);
$streamed->appendStream(
'unit_types',
$translator->trans("Loading Units..."),
fn() => $referenceSerialize->setClassName(Unit::class)->serializeCustomCore("unit_type")
);
$streamed->appendStream(
'product_catalog',
$translator->trans("Loading product catalog..."),
//fn() => array_map(
// fn(ProductCatalog $c) => [
// 'internal_code' => $c->getInternalCode(),
// 'internal_name' => $c->getInternalName(),
// 'description' => $c->getDescription(),
// ],
// $manager->getRepository(ProductCatalog::class)->findBy(['is_active' => true])
//)
fn() => $referenceSerialize->setEntity($manager->getRepository(ProductCatalog::class)->findAll())->setGroups([
"product_catalog@base"
])->render()
);
$streamed->appendStream(
'locations',
$translator->trans("Loading locations..."),
fn() => $referenceSerialize->setEntity($manager->getRepository(Locations::class)->findAll())->setGroups([
"location@base"
])->render()
);
//$streamed->appendStream(
// 'project_orders',
// $translator->trans("Loading orders..."),
// fn() => $projectOrderSerialize->setEntity($projectOrderService->fetchAll())->serializeForSelector()
//);
return $streamed->flush();
}
/**
* STREAMED POSITION
* @Route("/stream-expense-cart-dataset", name="api_v3_expense_stream_expense_chart_dataset")
* @throws \Exception
*/
public function streamExpenseCartDataset(ExpenseService $expenseService, TranslatorInterface $translator):
StreamedResponse {
$streamed = new StreamingService([
"localText" => [
"completed" => $translator->trans("Ready...")
]
]);
$streamed->appendStream(
'metrics',
$translator->trans("Loading metrics..."),
fn() => $expenseService->fetchChartDatasetMetrics()
);
$streamed->appendStream(
'by_project',
$translator->trans("Loading project breakdown..."),
fn() => $expenseService->fetchChartDatasetByProject()
);
$streamed->appendStream(
'by_supplier',
$translator->trans("Loading supplier breakdown..."),
fn() => $expenseService->fetchChartDatasetBySupplier()
);
$streamed->appendStream(
'orders',
$translator->trans("Loading orders..."),
fn() => $expenseService->fetchChartDatasetOrders()
);
$streamed->appendStream(
'by_day',
$translator->trans("Loading daily breakdown..."),
fn() => $expenseService->fetchChartDatasetByDay()
);
$streamed->appendStream(
'by_week',
$translator->trans("Loading weekly breakdown..."),
fn() => $expenseService->fetchChartDatasetByWeek()
);
return $streamed->flush();
}
/**
* @Route("/expenses/{page}/{limit}/{search}", name="api_v3_expense_expenses_paginated", methods={"POST"}, defaults={"page": 1, "limit": 5000, "search": null})
* @throws \Exception
*/
public function expensesPaginated(ExpenseService $expenseService, ExpenseSerialize $expenseSerialize, Request $request, TranslatorInterface $translator, int $page, int $limit, ?string $search):
Response
{
#dd($page, $limit, $search);
$pagination = $expenseService->fetchAllPaginated($page, $limit, $search);
$serializedExpenses = $expenseSerialize->setEntity($pagination->getItems())->serializeMainList();
#dd($serializedExpenses);
if ($expenseService->getException()) {
return $this->json([
"message" => $expenseService->getException()->getMessage(),
"severity" => SeverityInterface::WARNING
], Response::HTTP_BAD_REQUEST);
}
return $this->json([
"message" => $translator->trans('Loaded ranged expenses successfully'),
"severity" => SeverityInterface::SUCCESS,
"expenses" => $serializedExpenses,
"total" => $pagination->getTotalItemCount(),
"page" => $pagination->getCurrentPageNumber(),
"limit" => $pagination->getItemNumberPerPage(),
"totalPages" => (int) ceil($pagination->getTotalItemCount() / $pagination->getItemNumberPerPage()),
], Response::HTTP_OK);
}
/**
* Self-repair: recompute expense.total for every persisted expense from
* its current position data. User triggers this from the list menu when
* a total looks stale. Returns { message, severity, updated_count }.
*
* @Route("/sync-metrics", name="api_v3_expense_sync_metrics", methods={"POST"})
*/
public function syncMetrics(ExpenseService $expenseService, TranslatorInterface $translator): Response
{
$updated = $expenseService->syncAllExpenseTotals();
return $this->json([
'message' => sprintf('%d %s', $updated, $translator->trans('expense total(s) recomputed')),
'severity' => SeverityInterface::SUCCESS,
'updated_count' => $updated,
], Response::HTTP_OK);
}
/**
* Streamed variant of sync-metrics — SSE events per expense so the FE
* dialog can render a live progress bar with abort. Mirrors
* project sync-all-metrics pattern.
*
* @Route("/sync-metrics-streamed", name="api_v3_expense_sync_metrics_streamed", methods={"POST"})
*/
public function syncMetricsStreamed(ExpenseService $expenseService, Request $request): StreamedResponse
{
$syncToken = (string) $request->request->get('syncToken', uniqid('exp_sync_', true));
return new StreamedResponse(function () use ($expenseService, $syncToken) {
$emit = function (array $event) {
echo 'data: ' . json_encode($event) . "\n\n";
if (ob_get_level() > 0) {
ob_flush();
}
flush();
};
$expenseService->syncAllExpenseTotalsStreamed($syncToken, $emit);
}, Response::HTTP_OK, [
'Content-Type' => 'text/event-stream',
'Cache-Control' => 'no-cache, no-store',
'X-Accel-Buffering' => 'no',
'Connection' => 'keep-alive',
]);
}
/**
* Sibling to sync-metrics-streamed — writes an abort flag file that the
* running streamed sync polls each iteration. The dialog's Abort button
* hits this endpoint; the streamed loop then rolls back and emits an
* 'aborted' event so the client shell can close cleanly.
*
* @Route("/abort-sync", name="api_v3_expense_abort_sync", methods={"POST"})
*/
public function abortSync(ExpenseService $expenseService, Request $request): Response
{
$syncToken = (string) $request->request->get('syncToken', '');
if ($syncToken === '') {
return $this->json(['message' => 'syncToken is required', 'severity' => SeverityInterface::WARNING], Response::HTTP_BAD_REQUEST);
}
$expenseService->setAbortFlag($syncToken);
return $this->json(['message' => 'Abort flag set', 'severity' => SeverityInterface::SUCCESS], Response::HTTP_OK);
}
/**
* @deprecated Route disabled — replaced by expensesPaginated (KNP)
* // Route("/expenses/{page}/{limit}/{search}", name="api_v3_expense_expenses", methods={"POST"}, defaults={"page": 1, "limit": 5000, "search": null})
* @throws \Exception
*/
public function expenses( ExpenseService $expenseService, ExpenseSerialize $expenseSerialize, Request $request, TranslatorInterface $translator, int $page, int $limit, ?string $search ):
Response
{
$searchableColumns = $request->request->get('filterableColumns');
$searchableColumns = json_decode($searchableColumns);
$data = $expenseService->fetchAll($page, $limit, $search, $searchableColumns);
$serializedExpenses = $expenseSerialize->setEntity($data["data"])->serializeMainList();
if($expenseService->getException()){
return $this->json([
"message" => $expenseService->getException()->getMessage(),
"severity" => SeverityInterface::WARNING
], Response::HTTP_BAD_REQUEST );
}
return $this->json([
"message" => $translator->trans('Loaded ranged expenses successfully'),
"severity" => SeverityInterface::SUCCESS,
"expenses" => $serializedExpenses,
"total" => $data["total"]
], Response::HTTP_OK, [], [
// "groups" => $expenseSerialize->_groupFull()
]);
}
/**
* @Route("/fetch/{expense_id}", name="api_v3_expense_fetch", defaults={"expense_id", null})
* @ParamConverter("expense", options={"mapping": {"expense_id":"id"}})
* @throws \Exception
* STREAMED
*/
public function fetch(?ProjectOrderExpenses $expense, ExpenseSerialize $expenseSerialize, ReferenceService $referenceService, ReferenceSerialize $referenceSerialize, SupplierService $supplierService, SupplierSerialize $supplierSerialize,
TranslatorInterface $translator, int $expense_id = null
):
?StreamedResponse {
#dd($expense);
$streamed = new StreamingService([
"localText" => [
"completed" => $translator->trans("Ready...")
]
]);
$streamed->appendStream(
'expense',
$translator->trans("Loading expense..."),
fn() => $expense ? $expenseSerialize->setEntity($expense)->serializeFull() : []
);
$streamed->appendStream(
'paperPattern',
$translator->trans("Loading document pattern..."),
fn() => $referenceSerialize
->setEntity($referenceService->paperPattern( "expense"))
->serializePaperPattern()
);
$streamed->appendStream(
'suppliers',
$translator->trans("Loading suppliers..."),
fn() => $supplierSerialize
->setEntity($supplierService->fetchAll())
->serializeFull()
);
return $streamed->flush();
}
/**
* @Route("/upsert/{expense_id}", name="api_v3_expense_upsert", methods={"POST"}, defaults={"expense_id": null})
* @ParamConverter("expense", options={"mapping": {"expense_id": "id"}})
* @throws \Exception
*/
public function upsert(?ProjectOrderExpenses $expense, ExpenseService $expenseService, ExpenseSerialize $expenseSerialize, TranslatorInterface $translator, Request $request, ?int $expense_id ):
Response
{
$params = $request->request;
$files = $request->files;
$expense = $expenseService
->setParams($params)
->setFiles($files)
->setEntity($expense, $expense_id)->upsert();
if ($expenseService->getException()) {
return $this->json([
"message" => $expenseService->getException()->getMessage(),
"severity" => SeverityInterface::WARNING
], Response::HTTP_BAD_REQUEST);
}
$serializedExpense = $expenseSerialize->setEntity($expense)->serializeFull();
return $this->json([
"message" => $translator->trans('Updated invoices successfully'),
"severity" => "success",
"expense" => $serializedExpense
]);
}
/**
* ExpenseV2 — expense header + positions toplu upsert.
* Eski /upsert route'una dokunulmaz; bu route tam uyum sağlandığında eski silinir.
*
* @Route("/upsert-v2/{expense_id}", name="api_v3_expense_v2_upsert", methods={"POST"}, defaults={"expense_id": null})
* @ParamConverter("expense", options={"mapping": {"expense_id": "id"}})
* @throws \Exception
*/
public function upsertV2(
?ProjectOrderExpenses $expense,
ExpenseService $expenseService,
ExpenseSerialize $expenseSerialize,
TranslatorInterface $translator,
Request $request,
?int $expense_id
): Response {
$result = $expenseService
->setParams($request->request)
->setFiles($request->files)
->setEntity($expense, $expense_id)
->upsertV2();
if ($expenseService->getException()) {
return $this->json([
'message' => $expenseService->getException()->getMessage(),
'severity' => SeverityInterface::WARNING,
], Response::HTTP_BAD_REQUEST);
}
return $this->json([
'message' => $translator->trans('Expense saved successfully'),
'severity' => SeverityInterface::SUCCESS,
'expense' => $expenseSerialize->setEntity($result)->serializeFull(),
]);
}
/**
* @deprecated use syncPositions
* @Route("/remove/{expense_id}", name="api_v3_expense_remove", methods={"POST"}, defaults={"expense_id": null})
* @ParamConverter("expense", options={"mapping": {"expense_id": "id"}})
* @throws \Exception
*/
public function remove( ?ProjectOrderExpenses $expense, ExpenseService $expenseService, ExpenseSerialize $expenseSerialize, TranslatorInterface $translator, ?int $expense_id):
Response
{
#dd($expense_id);
$removedId = $expenseService->removeExpenseAndSync($expense);
if($expenseService->getException()){
return $this->json([
"message" => $expenseService->getException()->getMessage(),
"severity" => SeverityInterface::WARNING
], Response::HTTP_BAD_REQUEST );
}
return $this->json([
"message" => $translator->trans('Removed expense successfully'),
"severity" => SeverityInterface::SUCCESS,
"id" => $removedId
], Response::HTTP_OK);
}
/**
* TODO Depend On Expense Upsert (New Generation Model)
* TODO New Version Module
* @Route("/sync-positions/{expense_id}", name="api_v3_expense_sync_positions", methods={"POST"})
* @ParamConverter("invoice", options={"mapping": {"expense_id": "id"}})
* @throws \Exception
*/
public function syncPositions(ExpenseService $expenseService, ExpenseSerialize $expenseSerialize, TranslatorInterface $translator, ?int $invoice_id): Response
{
// TODO Wait for Development (Sync Depend On Expense)
return $this->json([
"message" => $translator->trans('Synced payments successfully'),
"severity" => "success",
// "invoice" => $serializedInvoice
// "amounts" => $amounts
]);
}
/**
* @throws \Exception
*@deprecated us SRP
* ❌TODO Temporarily Method (SRP) (Old generation model) (SRP Principal)
* TODO Positionslar Simdilik V0 Fetch ile hallediliyor yani
* Positionlar yerine Expense Fetch Ediliyor positionlar icinde
* @Route("/positions/{expense_id}", name="api_v3_expense_positions", methods={"POST"})
* @ParamConverter("expense", options={"mapping": {"expense_id": "id"}})
*/
public function positions( ProjectOrderExpenses $expense, ExpenseService $expenseService, ExpenseSerialize $expenseSerialize, TranslatorInterface $translator, ?int $expense_id):
Response
{
$serializedExpensePositions = $expenseSerialize
->setEntity($expense->getProjectOrderExpensePositions())
// ->setEntity($expense)
->serializePositionsFull();
// TODO Remove this byNew Version
$serializedExpense = $expenseSerialize
->setEntity($expense)
->serializePositionsFull();
if($expenseService->getException()){
return $this->json([
"message" => $expenseService->getException()->getMessage(),
"severity" => SeverityInterface::WARNING
], Response::HTTP_BAD_REQUEST );
}
return $this->json([
"message" => $translator->trans('Loaded expense positions successfully'),
"severity" => SeverityInterface::SUCCESS,
// "expensePositions" => $serializedExpensePositions,
// TODO No use by New Version
"expenseExpanded" => $serializedExpense,
"expense" => $serializedExpense,
"positions" => $serializedExpensePositions,
], Response::HTTP_OK);
}
/**
* @deprecated us SRP
* ❌TODO Maybe Temporarily Method (SRP) (Old generation model) (SRP Principal)
* @Route("/position-fetch/{expense_id}/{position_id}", name="api_v3_expense_position_fetch", methods={"POST"}, defaults={"position_id": null})
* @ParamConverter("expense", options={"mapping": {"expense_id": "id"}})
* @ParamConverter("expensePosition", options={"mapping": {"position_id": "id"}})
* @throws \Exception
*/
public function positionFetch( ProjectOrderExpenses $expense, ?ProjectOrderExpensePositions $expensePosition, ExpenseService $expenseService, ExpenseSerialize $expenseSerialize, Request $request, TranslatorInterface $translator, ?int $position_id):
Response
{
$expenseService
->setEntity($expensePosition, $position_id)
->setParams($request->request)
->setFiles($request->files)
->upsertPosition($expense);
if($expenseService->getException()){
return $this->json([
"message" => $expenseService->getException()->getMessage(),
"severity" => SeverityInterface::WARNING
], Response::HTTP_BAD_REQUEST );
}
$expenseExpanded = $expenseSerialize->setEntity($expensePosition->getProjectOrderExpense())->serializeFull();
return $this->json([
"message" => $translator->trans('Loaded expense positions successfully'),
"severity" => SeverityInterface::SUCCESS,
"expenseExpanded" => $expenseExpanded,
], Response::HTTP_OK);
}
/**
* STREAMED POSITION
* @Route("/position-streamed-fetch/{expense_id}/{position_id}", name="api_v3_expense_position_streamed_fetch", requirements={"expense_id":"\d+"}, defaults={"position_id":null})
* @ParamConverter("position", options={"mapping": {"position_id":"id"}})
* @ParamConverter("expense", options={"mapping": {"expense_id":"id"}})
* @throws \Exception
*/
public function positionStreamedFetch(
?ProjectOrderExpensePositions $position,
ProjectOrderExpenses $expense,
ExpenseSerialize $expenseSerialize,
MaterialService $materialService,
ReferenceSerialize $referenceSerialize,
SupplierService $supplierService,
SupplierSerialize $supplierSerialize,
EntityManagerInterface $manager,
SerializerInterface $serializer,
TranslatorInterface $translator
): StreamedResponse {
$streamed = new StreamingService([
"localText" => [
"completed" => $translator->trans("Ready...")
]
]);
$streamed->appendStream(
'position',
$translator->trans("Loading expense position..."),
fn() => $position ? $expenseSerialize->setEntity($position)->serializeFull() : []
);
$streamed->appendStream(
'expense',
$translator->trans("Loading expense..."),
fn() => $expense ? $expenseSerialize->setEntity($expense)->serializeFull() : []
);
// Materials with supplier relation for supplier-material mode
$streamed->appendStream(
'materials',
$translator->trans("Loading materials..."),
fn() => $referenceSerialize->setEntity($materialService->fetchAll())->setGroups([
"material@core",
"material@supplier",
"supplier@base",
])->render()
);
// Suppliers for supplier-material mode
$streamed->appendStream(
'suppliers',
$translator->trans("Loading suppliers..."),
fn() => $supplierSerialize->setEntity($supplierService->fetchAll())->serializeBase()
);
// Stock items for stock selection mode
$streamed->appendStream(
'stock_items',
$translator->trans("Loading stock items..."),
fn() => $serializer->normalize(
$manager->getRepository(StockItems::class)->findAll(),
null,
['groups' => ['stock_item@core']]
)
);
return $streamed->flush();
}
/**
* ❌TODO Temporarily Method (SRP) (Old generation model) (SRP Principal)
* @Route("/position-upsert/{expense_id}/{position_id}", name="api_v3_expense_position_upsert", methods={"POST"}, defaults={"position_id": null})
* @ParamConverter("expense", options={"mapping": {"expense_id": "id"}})
* @ParamConverter("expensePosition", options={"mapping": {"position_id": "id"}})
* @throws \Exception
*/
public function positionUpsert( ProjectOrderExpenses $expense, ?ProjectOrderExpensePositions $expensePosition, ExpenseService $expenseService, ExpenseSerialize $expenseSerialize, Request $request, TranslatorInterface $translator, ?int $position_id):
Response
{
$expensePosition = $expenseService
->setEntity($expensePosition, $position_id)
->setParams($request->request)
->setFiles($request->files)
->upsertPosition($expense);
if($expenseService->getException()){
return $this->json([
"message" => $expenseService->getException()->getMessage(),
"severity" => SeverityInterface::WARNING
], Response::HTTP_BAD_REQUEST );
}
# dd($expensePosition);
$expenseExpanded = $expenseSerialize->setEntity($expensePosition->getProjectOrderExpense())->serializeFull();
return $this->json([
"message" => $translator->trans('Loaded expense positions successfully'),
"severity" => SeverityInterface::SUCCESS,
"expenseExpanded" => $expenseExpanded,
], Response::HTTP_OK);
}
/**
* @Route("/position-remove/{position_id}", name="api_v3_expense_position_remove", methods={"POST"}, defaults={"position_id": null})
* @ParamConverter("position", options={"mapping": {"position_id": "id"}})
* @throws \Exception
*/
public function positionRemove( ?ProjectOrderExpensePositions $position, ExpenseService $expenseService, ExpenseSerialize $expenseSerialize, TranslatorInterface $translator, ?int $position_id):
Response
{
$parentExpense = $position->getProjectOrderExpense();
$removedId = $expenseService->deletePosition($position);
if($expenseService->getException()){
return $this->json([
"message" => $expenseService->getException()->getMessage(),
"severity" => SeverityInterface::WARNING
], Response::HTTP_BAD_REQUEST );
}
$expenseExpanded = $expenseSerialize->setEntity($parentExpense)->serializeFull();
return $this->json([
"message" => $translator->trans('Expense position removed successfully'),
"severity" => SeverityInterface::SUCCESS,
"id" => $removedId,
"expense" => $expenseExpanded
], Response::HTTP_OK);
}
/**
* @Route("/order-confirmation-pdf/{expense_id}", name="api_v3_expense_order_confirmation_pdf", methods={"POST"})
* @ParamConverter("expense", options={"mapping": {"expense_id": "id"}})
*/
public function orderConfirmationPdf(
ProjectOrderExpenses $expense,
Request $request,
ExpensePdfService $expensePdfService,
TranslatorInterface $translator
): Response {
$excludedIds = json_decode($request->request->get('excluded_position_ids', '[]'), true);
if (!is_array($excludedIds)) {
$excludedIds = [];
}
return $this->json([
'message' => $translator->trans('PDF generated successfully'),
'severity' => SeverityInterface::SUCCESS,
'base64' => $expensePdfService->generateOrderConfirmationPdf($expense, $excludedIds),
]);
}
/**
* @Route("/position-table-pdf/{expense_id}", name="api_v3_expense_position_table_pdf", methods={"POST"})
* @ParamConverter("expense", options={"mapping": {"expense_id": "id"}})
*/
public function positionDetailsPdfTable(
ProjectOrderExpenses $expense,
ExpensePdfService $expensePdfService,
TranslatorInterface $translator
): Response {
return $this->json([
'message' => $translator->trans('PDF generated successfully'),
'severity' => SeverityInterface::SUCCESS,
'base64' => $expensePdfService->generatePositionTablePdf($expense),
]);
}
/**
* @Route("/position-details-pdf/{expense_id}", name="api_v3_expense_position_details_pdf", methods={"POST"})
* @ParamConverter("expense", options={"mapping": {"expense_id": "id"}})
*/
public function positionDetailsPdf(
ProjectOrderExpenses $expense,
Request $request,
ExpensePdfService $expensePdfService,
TranslatorInterface $translator
): Response {
$positionIds = json_decode($request->request->get('position_ids', '[]'), true);
if (!is_array($positionIds)) {
$positionIds = [];
}
return $this->json([
'message' => $translator->trans('PDF generated successfully'),
'severity' => SeverityInterface::SUCCESS,
'base64' => $expensePdfService->generatePositionDetailsPdf($expense, $positionIds),
]);
}
/**
* Clears expensed_at (unlocks the expense). Requires high-level access — verified by FE before calling.
*
* @Route("/unlock/{expense_id}", name="api_v3_expense_unlock", methods={"POST"})
* @ParamConverter("expense", options={"mapping": {"expense_id": "id"}})
*/
public function unlock(
ProjectOrderExpenses $expense,
EntityManagerInterface $manager,
ExpenseSerialize $expenseSerialize,
TranslatorInterface $translator
): Response {
$expense->setExpensedAt(null);
$manager->flush();
return $this->json([
'message' => $translator->trans('Expense unlocked successfully'),
'severity' => SeverityInterface::SUCCESS,
'expense' => $expenseSerialize->setEntity($expense)->serializeFull(),
]);
}
/**
* @Route("/send-to-supplier/{expense_id}", name="api_v3_expense_send_to_supplier", methods={"POST"})
* @ParamConverter("expense", options={"mapping": {"expense_id": "id"}})
*/
public function sendToSupplier(
ProjectOrderExpenses $expense,
Request $request,
ExpensePdfService $expensePdfService,
MailerInterface $mailer,
EntityManagerInterface $manager,
ExpenseSerialize $expenseSerialize,
ExpenseService $expenseService,
TranslatorInterface $translator
): Response {
// Gonderim guard'i service'te: supplier email + zaten gonderilip kilitlenmis mi (expensed_at).
$expenseService->guardSendableToSupplier($expense);
if ($expenseService->getException() !== null) {
$exception = $expenseService->getException();
return $this->json([
'message' => $exception->getMessage(),
'severity' => SeverityInterface::WARNING,
], $exception->getCode());
}
$supplier = $expense->getSupplier();
$pdfType = $request->request->get('pdf_type', 'order_confirmation');
$excludedIds = json_decode($request->request->get('excluded_position_ids', '[]'), true);
if (!is_array($excludedIds)) {
$excludedIds = [];
}
$message = (string) ($request->request->get('message') !== null ? $request->request->get('message') : '');
if ($pdfType === 'order_confirmation') {
$base64 = $expensePdfService->generateOrderConfirmationPdf($expense, $excludedIds);
$subject = $translator->trans('Order Confirmation') . ' — ' . $expense->getExpenseNr();
$filename = 'order_confirmation_' . $expense->getExpenseNr() . '.pdf';
} else {
$base64 = $expensePdfService->generatePositionTablePdf($expense);
$subject = $translator->trans('Position Details') . ' — ' . $expense->getExpenseNr();
$filename = 'position_details_' . $expense->getExpenseNr() . '.pdf';
}
$pdfContent = base64_decode($base64);
$bodyHtml = nl2br(htmlspecialchars($message !== '' ? $message : $translator->trans('Please find the attached document.')));
$email = (new Email())
->from(new Address('einkauf@romon-erp.com', 'Romon Group'))
->to($supplier->getEmail())
->subject($subject)
->html($bodyHtml . '<br/><br/>')
->attach($pdfContent, $filename, 'application/pdf');
$mailer->send($email);
$expense->setExpensedAt(new \DateTimeImmutable());
$manager->flush();
return $this->json([
'message' => $translator->trans('Document sent successfully'),
'severity' => SeverityInterface::SUCCESS,
'expense' => $expenseSerialize->setEntity($expense)->serializeFull(),
]);
}
/**
* @Route("/generate-document-paths/{expense_id}/{position_id}", name="api_v3_expense_generate_document_paths", methods={"POST"}, defaults={"expense_id": null, "position_id": null})
* @ParamConverter("expense", options={"mapping": {"expense_id": "id"}})
* @ParamConverter("position", options={"mapping": {"position_id": "id"}})
* @throws \Exception
*/
public function generateDocumentAbsolutePath(?ProjectOrderExpenses $expense, ?ProjectOrderExpensePositions $position, TranslatorInterface $translator, ExpenseService $expenseService):
Response
{
if(!$expense && $position){
throw new \Exception("File path generator two of one (Expense or position required)");
}
// TODO All 2 Way should be array getFileName -> documents
$files = $position ?
$position->getDocuments()
: $expense->getDocuments();
#dump($files);
$paths = $expenseService->generateDocumentAbsolutePath($files);
#dd($paths);
// TODO Wait for Development (Sync Depend On Expense)
return $this->json([
"message" => $translator->trans('Files generated successfully'),
"severity" => "success",
"paths" => $paths,
"headerName" => $position ? $position->getProjectOrder()->getOrderNr() : $expense->getExpenseNr()
]);
}
}