<?php
namespace App\Controller\Application;
use App\Builder\ApplicationCommentBuilder;
use App\BusinessManager\ApplicationBusinessManager;
use App\Constant\DocumentType;
use App\Controller\BaseController;
use App\Form\Helper\FormHelper;
use App\Form\Model\AcquiringModel;
use App\Form\Model\Application\ManualRiskFactor;
use App\Form\Model\ApplicationFilterModel;
use App\Form\Model\ApplicationModel;
use App\Form\Model\ExportApplicationModel;
use App\Form\Type\Application\ManualRiskFactorType;
use App\Form\Type\Application\WorkflowType;
use App\Form\Type\ApplicationFilterType;
use App\Form\Type\ApplicationJsonType;
use App\Form\Type\ApplicationType;
use App\Form\Type\ExportApplicationType;
use App\Form\Type\User\UserCommentType;
use App\Repository\ApplicationClickHouseRepository;
use App\Repository\UserRepository;
use App\RequestManager\Account\BalanceRequestManager;
use App\RequestManager\Application\ApplicationProductStatusRequestManager;
use App\RequestManager\Application\ApplicationRequestManager;
use App\RequestManager\Application\CompanyRequestManager;
use App\RequestManager\IdentificationRequestManager;
use App\RequestManager\InstanceRequestManager;
use App\RequestManager\PayooRequestManager;
use App\RequestManager\RiskV3RequestManager;
use GuzzleHttp\Exception\GuzzleException;
use OldSound\RabbitMqBundle\RabbitMq\ProducerInterface;
use Paynetics\Exception\ApiException;
use Paynetics\Exception\RequestException;
use Symfony\Component\HttpFoundation\HeaderUtils;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Serializer\Exception\ExceptionInterface;
/**
* Class ApplicationController.
*/
class ApplicationController extends BaseController
{
/**
* @required
*/
public PayooRequestManager $payooRequestManager;
/**
* @required
*/
public ApplicationRequestManager $applicationRequestManager;
/**
* @required
*/
public ApplicationProductStatusRequestManager $applicationProductStatusRequestManager;
/**
* @required
*/
public IdentificationRequestManager $identificationService;
/**
* @required
*/
public UserRepository $userRepository;
/**
* @required
*/
public ApplicationClickHouseRepository $applicationRepository;
/**
* @required
*/
public RiskV3RequestManager $riskRequestManager;
/**
* @required
*/
public BalanceRequestManager $balanceRequestManager;
/**
* @required
*/
public InstanceRequestManager $instanceRequestManager;
/**
* @required
*/
public ProducerInterface $applicationWorkflowProducer;
/**
* @required
*/
public ProducerInterface $riskLeProducer;
/**
* @required
*/
public RiskV3RequestManager $riskV3RequestManager;
/**
* @required
*/
public CompanyRequestManager $companyRequestManager;
/**
* @required
*/
public ApplicationBusinessManager $applicationBusinessManager;
/**
* @throws ApiException
* @throws GuzzleException
* @throws \JsonException
* @throws ExceptionInterface
*/
public function index(Request $request, int $page = 1, int $limit = 20): Response
{
$this->isGranted(['ROLE_SUPPORT', 'ROLE_KYB', 'ROLE_CARD_OPS', 'ROLE_DISPUTE', 'ROLE_PM', 'ROLE_READ_ONLY']);
$filter = $this->createForm(ApplicationFilterType::class, new ApplicationFilterModel(), ['action' => $this->generateUrl('applications')]);
$filter->handleRequest($request);
$applicationsData = $this->applicationRepository->getApplications($page, $limit, $filter->getData());
$applications = $this->paginate($applicationsData, $page, $limit);
// Create export form
$exportModel = new ExportApplicationModel();
$exportForm = $this->createForm(ExportApplicationType::class, $exportModel, [
'action' => $this->generateUrl('export'),
]);
return $this->render('application/index.html.twig', [
'applications' => $applications,
'current' => $page,
'page' => $page,
'filter' => $filter->createView(),
'exportForm' => $exportForm->createView(),
]);
}
public function create(Request $request): Response
{
$this->isGranted(['ROLE_SUPPORT', 'ROLE_KYB']);
// Create the form using our new ApplicationJsonType
$form = $this->createForm(ApplicationJsonType::class);
$form->handleRequest($request);
if ($form->isSubmitted()) {
$data = $form->getData();
foreach ($data['company']['company_members'] as $id => $companyMember) {
if (true == $companyMember['is_deleted']) {
unset($data['company']['company_members'][$id]);
}
}
if ($form->isValid()) {
try {
// Process company members' documents
if (isset($data['company']['company_members'])) {
foreach ($data['company']['company_members'] as &$member) {
if (isset($member['documents'])) {
// Filter out empty document entries
$member['documents'] = array_filter($member['documents'], function ($doc) {
return !empty($doc) && isset($doc['file']);
});
// Reset array keys to be sequential
$member['documents'] = array_values($member['documents']);
foreach ($member['documents'] as &$document) {
if (isset($document['file']) && $document['file'] instanceof \Symfony\Component\HttpFoundation\File\UploadedFile) {
// Get file extension BEFORE converting to base64
$document['file_type'] = pathinfo($document['file']->getClientOriginalName(), PATHINFO_EXTENSION);
// Convert file to base64
$document['file'] = base64_encode(file_get_contents($document['file']->getPathname()));
}
}
}
}
}
// Process company documents
if (isset($data['company']['documents'])) {
// Filter out empty document entries
$data['company']['documents'] = array_filter($data['company']['documents'], function ($doc) {
return !empty($doc) && isset($doc['file']);
});
// Reset array keys to be sequential
$data['company']['documents'] = array_values($data['company']['documents']);
foreach ($data['company']['documents'] as &$document) {
if (isset($document['file']) && $document['file'] instanceof \Symfony\Component\HttpFoundation\File\UploadedFile) {
// Get file extension BEFORE converting to base64
$document['file_type'] = pathinfo($document['file']->getClientOriginalName(), PATHINFO_EXTENSION);
// Convert file to base64
$document['file'] = base64_encode(file_get_contents($document['file']->getPathname()));
}
}
}
// Set IP address and fingerprint
$data['ip_address'] = $request->getClientIp();
$data['fingerprint'] = $request->headers->get('User-Agent');
$data['channel'] = 'admin';
// Create the application
$result = $this->applicationRequestManager->createApplication($data);
$this->addFlash('success', 'Application created successfully');
return $this->redirectToRoute('application_view', [
'token' => $result['token'] ?? null,
'product' => $data['products'][0] ?? 'pos',
]);
} catch (\Exception $e) {
// TODO remove before deploy on prod
$errorMessage = 'Error creating application: '.$e->getMessage();
// $this->addFlash('error', 'Error creating application: ' . $e->getMessage());
}
}
}
return $this->renderForm('application/create.html.twig', [
'form' => $form,
'errorMessage' => $errorMessage ?? null,
]);
}
/**
* @throws ApiException
* @throws GuzzleException
* @throws \JsonException
*/
public function view(string $token, string $product): Response
{
$this->isGranted(['ROLE_SUPPORT', 'ROLE_KYB', 'ROLE_CARD_OPS', 'ROLE_DISPUTE', 'ROLE_PM']);
$application = $this->applicationRequestManager->getApplication($token);
$aps = array_filter($application['application_product_statuses'] ?? [], function ($aps) use ($product) {
return ($aps['product']['name'] ?? null) === $product;
});
if (empty($aps)) {
throw new \Exception('Product '.$product.' not found for application');
}
$aps = end($aps);
$documents = $this->identificationService->getDocumentsList($application['company']['token'], 'company', 'create');
$documentTypes = array_column(
$this->identificationService->getTypes()['items'] ?? [],
'name',
'code'
);
return $this->render('application/show.html.twig', [
'application' => $application,
'aps' => $aps,
'product' => $product,
'documents' => $documents,
'documentTypes' => $documentTypes,
]);
}
public function merchant(string $token): RedirectResponse
{
try {
$application = $this->applicationRequestManager->getByMerchant($token);
$product = $application['application_product_statuses'][0]['product']['name'];
} catch (\Throwable $e) {
return $this->redirectToRoute('merchants_show', ['token' => $token]);
}
return $this->redirectToRoute('application_view', ['token' => $application['token'], 'product' => $product]);
}
/**
* @throws ApiException
* @throws GuzzleException
*/
public function reassign(string $token, string $product, string $admin): RedirectResponse
{
$this->applicationProductStatusRequestManager->update($token, $product, ['reviewer' => $admin]);
return $this->redirectToRoute('application_view', compact('token', 'product'));
}
/**
* @throws ApiException
* @throws GuzzleException
* @throws \JsonException
*/
public function additionalInfo(Request $request, string $token, string $product): Response
{
$application = $this->applicationRequestManager->getApplication($token);
$aps = array_filter($application['application_product_statuses'] ?? [], function ($aps) use ($product) {
return ($aps['product']['name'] ?? null) === $product;
});
if (empty($aps)) {
throw new \Exception('Product '.$product.' not found for application');
}
$aps = end($aps);
if (isset($application['comments']) && is_array($application['comments'])) {
$tokens = [];
foreach ($application['comments'] as $comment) {
if (isset($comment['comment_by']) && !empty($comment['comment_by'])) {
$tokens[] = $comment['comment_by'];
}
}
$usernameMap = $this->userRepository->findUsernamesByTokens(array_unique($tokens));
foreach ($application['comments'] as &$comment) {
if (isset($comment['comment_by']) && isset($usernameMap[$comment['comment_by']])) {
$comment['comment_by'] = $usernameMap[$comment['comment_by']];
} else {
$comment['comment_by'] = 'Unknown User';
}
}
}
$commentForm = $this->createForm(UserCommentType::class);
$commentForm->handleRequest($request);
if ($commentForm->isSubmitted() && $commentForm->isValid()) {
$commentData = $commentForm->getData();
$comment = $commentData->getComment();
if (!empty($comment)) {
$data = ApplicationCommentBuilder::createComment($token, $this->getUser()->getToken(), $comment);
$this->applicationRequestManager->createComment($data);
$this->addFlash('success', 'Comment added successfully');
return $this->redirectToRoute('application_additional_info', [
'token' => $token,
'product' => $product,
]);
}
}
return $this->render('application/additional-info.html.twig', [
'application' => $application,
'aps' => $aps,
'product' => $product,
'commentForm' => $commentForm->createView(),
]);
}
/**
* @throws ApiException
* @throws GuzzleException
* @throws \JsonException
*/
public function statusLogs(string $token, string $product, int $page = 1, int $limit = 20): Response
{
$this->isGranted(['ROLE_SUPPORT', 'ROLE_KYB', 'ROLE_DISPUTE']);
$application = $this->applicationRequestManager->getApplication($token);
$logs = $this->applicationRequestManager->statusLogs($token, $page, $limit);
foreach ($logs['items'] as &$log) {
if (isset($log['admin'])) {
$log['admin'] = $this->userRepository->findOneBy(['token' => $log['admin']])->getUsername();
} else {
$log['admin'] = 'Automatically';
}
}
$aps = array_filter($application['application_product_statuses'] ?? [], function ($aps) use ($product) {
return ($aps['product']['name'] ?? null) === $product;
});
if (empty($aps)) {
throw new \Exception('Product '.$product.' not found for application');
}
$aps = end($aps);
return $this->render('application/status-logs.html.twig', compact('application', 'logs', 'product', 'aps'));
}
/**
* @throws ApiException
* @throws GuzzleException
* @throws \JsonException
*/
public function mccAuditLogs(string $token, string $product, int $page = 1, int $limit = 20): Response
{
$this->isGranted(['ROLE_SUPPORT', 'ROLE_KYB', 'ROLE_DISPUTE']);
$application = $this->applicationRequestManager->getApplication($token);
$logs = $this->applicationRequestManager->mccAuditLogs($token, $page, $limit);
foreach ($logs['items'] as &$log) {
if (isset($log['admin'])) {
$user = $this->userRepository->findOneBy(['token' => $log['admin']]);
$log['admin'] = $user ? $user->getUsername() : $log['admin'];
} else {
$log['admin'] = 'Automatically';
}
}
$aps = array_filter($application['application_product_statuses'] ?? [], function ($aps) use ($product) {
return ($aps['product']['name'] ?? null) === $product;
});
if (empty($aps)) {
throw new \Exception('Product '.$product.' not found for application');
}
$aps = end($aps);
$documentTypes = array_flip(DocumentType::DOCUMENT_TYPES_NAMES);
return $this->render('application/mcc-audit-logs.html.twig', compact('application', 'logs', 'documentTypes', 'product', 'aps'));
}
/**
* @throws ApiException
* @throws GuzzleException
* @throws \JsonException
*/
public function workflow(Request $request, string $token, string $product): Response
{
$this->isGranted(['ROLE_DEVELOPER', 'ROLE_DISPUTE']);
$application = $this->applicationRequestManager->getApplication($token);
$workflow = $this->applicationRequestManager->workflow($token);
$aps = array_filter($application['application_product_statuses'] ?? [], function ($aps) use ($product) {
return ($aps['product']['name'] ?? null) === $product;
});
if (empty($aps)) {
throw new \Exception('Product '.$product.' not found for application');
}
$aps = end($aps);
$documents = $this->identificationService->getDocumentsList($application['company']['token'], 'company', 'create');
$documentTypes = array_flip(DocumentType::DOCUMENT_TYPES_NAMES);
$form = $this->createForm(WorkflowType::class, null, ['instance' => $application['instance']]);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$this->applicationWorkflowProducer->publish(json_encode([
'application' => $token,
'criteria' => $form->getData()['criteria'],
], JSON_THROW_ON_ERROR));
}
$form = $form->createView();
return $this->render('application/workflow.html.twig', compact('application', 'workflow', 'aps', 'product', 'documents', 'documentTypes', 'form'));
}
/**
* @throws ApiException
* @throws GuzzleException
* @throws \JsonException
* @throws \Exception
*/
public function risk(Request $request, string $token, string $product): Response
{
$this->isGranted(['ROLE_SUPPORT', 'ROLE_KYB', 'ROLE_CARD_OPS', 'ROLE_DISPUTE', 'ROLE_PM']);
$application = $this->applicationRequestManager->getApplication($token);
$aps = array_filter($application['application_product_statuses'] ?? [], static function ($aps) use ($product) {
return ($aps['product']['name'] ?? null) === $product;
});
if (empty($aps)) {
throw new \Exception('Product '.$product.' not found for application');
}
$aps = end($aps);
$manualRiskFromModel = new ManualRiskFactor();
$manualRiskFromModel->setBehaviour($application['behaviour']);
$manualRiskFromModel->setReputation($application['reputation']);
$manualRiskFromModel->setPostSar($application['post_sar']);
$manualRiskFromModel->setSanctionTfMatch($application['sanction_tf_match']);
$manualRiskFromModel->setUboBehaviour($application['ubo_behaviour']);
$manualRiskFromModel->setUboReputation($application['ubo_reputation']);
$manualRiskFromModel->setUboPostSar($application['ubo_post_sar']);
$manualRiskFromModel->setUboSanctionTfMatch($application['ubo_sanction_tf_match']);
$manualRiskFrom = $this->createForm(ManualRiskFactorType::class, $manualRiskFromModel);
$manualRiskFrom->handleRequest($request);
if ($manualRiskFrom->isSubmitted() && $manualRiskFrom->isValid()) {
/**
* @var ManualRiskFactor $formData
*/
$formData = $manualRiskFrom->getData();
$data = [];
$data['reputation'] = $formData->getReputation();
$data['behaviour'] = $formData->getBehaviour();
$data['post_sar'] = $formData->getPostSar();
$data['sanction_tf_match'] = $formData->getSanctionTfMatch();
$data['ubo_reputation'] = $formData->getUboReputation();
$data['ubo_behaviour'] = $formData->getUboBehaviour();
$data['ubo_post_sar'] = $formData->getUboPostSar();
$data['ubo_sanction_tf_match'] = $formData->getUboSanctionTfMatch();
$data['instance'] = $application['instance'];
$data['due_diligence'] = $application['due_diligence'] ?? null;
$this->applicationRequestManager->updateV2($token, $data);
$this->riskLeProducer->publish(json_encode([
'application' => $token,
], JSON_THROW_ON_ERROR));
return $this->redirectToRoute('application_risk', ['token' => $token, 'product' => $product]);
}
$documents = $this->identificationService->getDocumentsList($application['company']['token'], 'company', 'create');
if(empty($application['merchant'])) {
$risk = null;
} else {
$risk = $this->riskRequestManager->loadApplicationScore($application['merchant']);
}
return $this->render('application/risk.html.twig', [
'application' => $application,
'application_token' => $token,
'aps' => $aps,
'product' => $product,
'documents' => $documents,
'risk' => $risk ?? null,
'manualRiskForm' => $manualRiskFrom->createView(),
]);
}
/**
* @throws ApiException
* @throws GuzzleException
*/
public function downloadRiskFile(Request $request, ?string $token = null): Response
{
$this->isGranted(['ROLE_KYC', 'ROLE_KYB', 'ROLE_TRANSFERS', 'ROLE_SUPPORT', 'ROLE_SUPPORT_MANAGER', 'ROLE_TRANSFERS_MANAGER', 'ROLE_TRANSACTION_MANAGER', 'ROLE_CARD_OPS', 'ROLE_CARD_OPS_MANAGER', 'ROLE_RISK_MANAGER', 'ROLE_DISPUTE']);
$response = new Response($this->riskV3RequestManager->downloadRiskFile(null, $token));
$disposition = HeaderUtils::makeDisposition(
HeaderUtils::DISPOSITION_ATTACHMENT,
'risk-score-'.$token.'.xlsx'
);
$response->headers->set('Content-Disposition', $disposition);
return $response;
}
/**
* @throws ApiException
* @throws ExceptionInterface
* @throws GuzzleException
* @throws RequestException
* @throws \JsonException
*/
public function update(Request $request, string $token, string $product): Response
{
$this->isGranted(['ROLE_KYB']);
try {
$applicationData = $this->applicationRequestManager->getApplication($token);
} catch (RequestException $e) {
if (11002 === $e->getCode()) {
return new Response($e->getMessage(), 404);
}
throw $e;
}
// Extract Application Product Status (aps) for navigation
$aps = array_filter($applicationData['application_product_statuses'] ?? [], function ($aps) use ($product) {
return ($aps['product']['name'] ?? null) === $product;
});
$aps = !empty($aps) ? end($aps) : null;
if (isset($applicationData['company']['incorporation_date'])) {
$dateTime = new \DateTime($applicationData['company']['incorporation_date']);
$applicationData['company']['incorporation_date'] = $dateTime->format('Y-m-d');
}
// Capture due_diligence before denormalization (snake_case from API)
$dueDiligence = $applicationData['due_diligence'] ?? null;
$acquiringData = [];
if (isset($applicationData['acquiring']) && is_array($applicationData['acquiring'])) {
foreach ($applicationData['acquiring'] as $acquiring) {
$acquiringModel = new AcquiringModel();
$acquiringModel->setType($acquiring['type'] ?? null);
$acquiringModel->setFeeGroup($acquiring['fee_group'] ?? null);
$acquiringModel->setSupportedAuthorizationTypes($acquiring['supported_authorization_types'] ?? []);
$acquiringModel->setDescriptor($acquiring['descriptor'] ?? null);
if (($acquiring['type'] ?? '') === 'e-commerce' || ($acquiring['type'] ?? '') === 'pos') {
$acquiringModel->setRegister3ds($acquiring['register3ds'] ?? false);
}
$acquiringData[] = $acquiringModel;
}
}
/**
* @var ApplicationModel $applicationModel
*/
$applicationModel = $this->denormalize($applicationData ?? [], ApplicationModel::class);
$applicationModel->setProducts([$product]);
$applicationModel->setAcquiring($acquiringData);
$applicationModel->setDueDiligence($dueDiligence);
$form = $this->createForm(ApplicationType::class, $applicationModel);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
try {
$this->applicationBusinessManager->updateFromModel($token, $form->getData(), $request->get('syncWithBanks', false));
return $this->redirectToRoute('application_view', ['token' => $token, 'product' => $product]);
} catch (RequestException $e) {
if (11001 === $e->getCode()) {
foreach ($e->getErrors() as $key => $message) {
FormHelper::addFieldError($form, $key, $message);
}
}
}
}
return $this->renderForm('application/company/update.html.twig', [
'form' => $form,
'application' => $applicationData,
'product' => $product,
'aps' => $aps,
'token' => $token,
]);
}
/**
* @return RedirectResponse
*/
public function updateCompanySingle($token, $product, $company, $field, $value)
{
$this->isGranted(['ROLE_KYB']);
$data['token'] = $company;
$data[$field] = $value;
try {
$this->applicationRequestManager->updateCompany($token, $data);
} catch (GuzzleException|ApiException $e) {
}
return $this->redirectToRoute('application_view_trade_register', ['token' => $token, 'product' => $product]);
}
/**
* @throws ApiException
* @throws GuzzleException
*/
public function updateStatus(string $token, string $status, string $product): RedirectResponse
{
$this->isGranted(['ROLE_KYB']);
$this->applicationRequestManager->updateApplicationStatus($token, $status, $product, $this->getUser()->getToken());
return $this->redirectToRoute('application_view', ['token' => $token, 'product' => $product]);
}
}