src/ApplicationBundle/Modules/BankConnect/Controller/BankConnectController.php line 33

Open in your IDE?
  1. <?php
  2. namespace ApplicationBundle\Modules\BankConnect\Controller;
  3. use ApplicationBundle\Controller\GenericController;
  4. use ApplicationBundle\Entity\FinancialConnector;
  5. use ApplicationBundle\Interfaces\SessionCheckInterface;
  6. use ApplicationBundle\Modules\Aspire\Aspire;
  7. use ApplicationBundle\Modules\Authentication\Constants\UserConstants;
  8. use ApplicationBundle\Modules\BankConnect\ConnectorCredentialService;
  9. use ApplicationBundle\Modules\BankConnect\ConnectorRegistry;
  10. use Symfony\Component\HttpFoundation\JsonResponse;
  11. use Symfony\Component\HttpFoundation\Request;
  12. use Symfony\Component\HttpFoundation\Response;
  13. /**
  14.  * BC2 — the tenant-facing "connect a bank" UI over the unified connector registry
  15.  * ({@see FinancialConnector}). One screen for every provider: list connectors + status, add/edit
  16.  * one, pause/resume, delete, and sync-now. Credentials never leave this box in the clear —
  17.  * secrets are encrypted on save ({@see ConnectorCredentialService}) and only ever shown redacted.
  18.  *
  19.  * Aspire (adapter #1) keeps its detailed setup in the Aspire module; here it is "linked" by
  20.  * pointing a registry row at an existing AspireConnection, so the unified screen can drive it
  21.  * (sync-now → the proven Aspire::syncConnection) without duplicating its credentials.
  22.  */
  23. class BankConnectController extends GenericController implements SessionCheckInterface
  24. {
  25.     private function boot(): void
  26.     {
  27.         ConnectorCredentialService::ensureKey($this->container);
  28.     }
  29.     public function indexAction(Request $request): Response
  30.     {
  31.         $this->boot();
  32.         $em $this->getDoctrine()->getManager();
  33.         $companyId $request->getSession()->get(UserConstants::USER_COMPANY_ID);
  34.         $connectors $em->getRepository('ApplicationBundle\\Entity\\FinancialConnector')
  35.             ->findBy(array('companyId' => $companyId), array('id' => 'DESC'));
  36.         // Existing Aspire connections available to link (so the tenant picks, never hand-types).
  37.         $aspireConnections $em->getRepository('ApplicationBundle\\Entity\\AspireConnection')
  38.             ->findBy(array('companyId' => $companyId), array('id' => 'DESC'));
  39.         return $this->render('@BankConnect/pages/bank_connect.html.twig', array(
  40.             'connectors'        => $connectors,
  41.             'aspireConnections' => $aspireConnections,
  42.             'builtProviders'    => ConnectorRegistry::BUILT,
  43.             'plannedProviders'  => ConnectorRegistry::PLANNED,
  44.             'keyAvailable'      => ConnectorCredentialService::keyAvailable(),
  45.             'page_title'        => 'Bank Connections',
  46.         ));
  47.     }
  48.     /** POST /bank-connect/save — create or update one connector. */
  49.     public function saveAction(Request $request): JsonResponse
  50.     {
  51.         $this->boot();
  52.         $em $this->getDoctrine()->getManager();
  53.         $companyId = (int) $request->getSession()->get(UserConstants::USER_COMPANY_ID);
  54.         $id       = (int) $request->request->get('id'0);
  55.         $provider strtolower(trim((string) $request->request->get('provider''')));
  56.         $label    trim((string) $request->request->get('label'''));
  57.         $schedule trim((string) $request->request->get('schedule_cron'''));
  58.         if ($provider === '') {
  59.             return new JsonResponse(array('success' => false'message' => 'Provider is required.'));
  60.         }
  61.         if (!in_array($providerConnectorRegistry::knownProviders(), true)) {
  62.             return new JsonResponse(array('success' => false'message' => 'Unknown provider.'));
  63.         }
  64.         if ($id) {
  65.             $c $em->getRepository('ApplicationBundle\\Entity\\FinancialConnector')->find($id);
  66.             if (!$c || (int) $c->getCompanyId() !== $companyId) {
  67.                 return new JsonResponse(array('success' => false'message' => 'Not found.'));
  68.             }
  69.         } else {
  70.             $c = new FinancialConnector();
  71.             $c->setCompanyId($companyId ?: 1);
  72.             $c->setStatus(FinancialConnector::STATUS_ACTIVE);
  73.             $c->setCreatedAt(new \DateTime());
  74.         }
  75.         $c->setProvider($provider);
  76.         $c->setLabel($label !== '' $label ucfirst($provider));
  77.         $c->setScheduleCron($schedule !== '' $schedule null);
  78.         $c->setUpdatedAt(new \DateTime());
  79.         if ($provider === 'aspire') {
  80.             // Aspire creds live on the AspireConnection — link, don't duplicate.
  81.             $linked = (int) $request->request->get('linked_aspire_connection_id'0);
  82.             if ($linked <= 0) {
  83.                 return new JsonResponse(array('success' => false'message' => 'Choose an Aspire connection to link.'));
  84.             }
  85.             $ac $em->getRepository('ApplicationBundle\\Entity\\AspireConnection')->find($linked);
  86.             if (!$ac || (int) $ac->getCompanyId() !== $companyId) {
  87.                 return new JsonResponse(array('success' => false'message' => 'Aspire connection not found.'));
  88.             }
  89.             $c->setAuthType(FinancialConnector::AUTH_API_KEY);
  90.             $c->setLinkedAspireConnectionId($linked);
  91.         } else {
  92.             // Other providers: creds are entered here → ENCRYPTED at rest. Refuse to store in the
  93.             // clear (fail closed) if no encryption key is configured on the box.
  94.             $credsJson trim((string) $request->request->get('credentials'''));
  95.             if ($credsJson !== '') {
  96.                 if (!ConnectorCredentialService::keyAvailable()) {
  97.                     return new JsonResponse(array('success' => false'message' => 'Encryption key not configured — cannot store credentials securely. Set app_encryption_key first.'));
  98.                 }
  99.                 $creds json_decode($credsJsontrue);
  100.                 if (!is_array($creds)) {
  101.                     // Accept a bare token too (e.g. Wise API token pasted directly).
  102.                     $creds = array('token' => $credsJson);
  103.                 }
  104.                 $c->setCredentialsEnc(ConnectorCredentialService::encrypt($creds));
  105.             }
  106.             $c->setAuthType($provider === 'wise' FinancialConnector::AUTH_API_KEY FinancialConnector::AUTH_OAUTH2);
  107.         }
  108.         $accountRefs trim((string) $request->request->get('account_refs'''));
  109.         if ($accountRefs !== '') { $c->setAccountRefs($accountRefs); }
  110.         $em->persist($c);
  111.         $em->flush();
  112.         return new JsonResponse(array('success' => true'id' => $c->getId()));
  113.     }
  114.     /** POST /bank-connect/{id}/toggle — pause ⇄ resume. */
  115.     public function toggleAction(Request $requestint $id): JsonResponse
  116.     {
  117.         $this->boot();
  118.         $c $this->ownedConnector($request$id);
  119.         if (!$c) { return new JsonResponse(array('success' => false'message' => 'Not found.')); }
  120.         $c->setStatus($c->getStatus() === FinancialConnector::STATUS_ACTIVE
  121.             FinancialConnector::STATUS_PAUSED FinancialConnector::STATUS_ACTIVE);
  122.         $c->setUpdatedAt(new \DateTime());
  123.         $this->getDoctrine()->getManager()->flush();
  124.         return new JsonResponse(array('success' => true'status' => $c->getStatus()));
  125.     }
  126.     /** POST /bank-connect/{id}/delete. */
  127.     public function deleteAction(Request $requestint $id): JsonResponse
  128.     {
  129.         $this->boot();
  130.         $c $this->ownedConnector($request$id);
  131.         if (!$c) { return new JsonResponse(array('success' => false'message' => 'Not found.')); }
  132.         $em $this->getDoctrine()->getManager();
  133.         $em->remove($c);
  134.         $em->flush();
  135.         return new JsonResponse(array('success' => true));
  136.     }
  137.     /** POST /bank-connect/{id}/sync — sync one connector now (Aspire → proven syncConnection). */
  138.     public function syncNowAction(Request $requestint $id): JsonResponse
  139.     {
  140.         $this->boot();
  141.         $c $this->ownedConnector($request$id);
  142.         if (!$c) { return new JsonResponse(array('success' => false'message' => 'Not found.')); }
  143.         $em $this->getDoctrine()->getManager();
  144.         $loginId = (int) $request->getSession()->get(UserConstants::USER_LOGIN_ID);
  145.         if (strtolower((string) $c->getProvider()) === 'aspire') {
  146.             $ac $c->getLinkedAspireConnectionId()
  147.                 ? $em->getRepository('ApplicationBundle\\Entity\\AspireConnection')->find((int) $c->getLinkedAspireConnectionId())
  148.                 : null;
  149.             if (!$ac) { return new JsonResponse(array('success' => false'message' => 'Linked Aspire connection missing.')); }
  150.             $res Aspire::syncConnection($ac$em$loginId'connect-ui');
  151.             $c->setLastSyncAt(new \DateTime());
  152.             $c->setLastSyncStatus(!empty($res['success']) ? 'ok' 'error');
  153.             $c->setLastSyncError(!empty($res['success']) ? null : ($res['error'] ?? 'sync error'));
  154.             $em->flush();
  155.             if (empty($res['success'])) {
  156.                 return new JsonResponse(array('success' => false'message' => $res['error'] ?? 'Sync failed.'));
  157.             }
  158.             return new JsonResponse(array('success' => true'detail' => sprintf('Fetched %d, new %d, posted %d.',
  159.                 $res['fetched'] ?? 0$res['new'] ?? 0$res['auto_posted'] ?? 0)));
  160.         }
  161.         return new JsonResponse(array('success' => false'message' => ucfirst($c->getProvider()) . ' sync ships in a later phase (BC3/BC4).'));
  162.     }
  163.     private function ownedConnector(Request $requestint $id)
  164.     {
  165.         $companyId = (int) $request->getSession()->get(UserConstants::USER_COMPANY_ID);
  166.         $c $this->getDoctrine()->getManager()->getRepository('ApplicationBundle\\Entity\\FinancialConnector')->find($id);
  167.         return ($c && (int) $c->getCompanyId() === $companyId) ? $c null;
  168.     }
  169. }