sample code

This commit is contained in:
2022-10-13 11:55:19 -04:00
commit bde8aa5e5d
67 changed files with 6421 additions and 0 deletions
@@ -0,0 +1,9 @@
name: Dafter Stock API
description: Provides services to fetch stock related data from various third-party APIs.
core: 8.x
type: module
package: Daftar API
core_version_requirement: ^8 || ^9
version: 1.0.0
tags:
- api
@@ -0,0 +1,15 @@
services:
daftar.stock_api.alphavantage:
class: Drupal\daftar_stock_api\Service\AlphaVantageApiService
arguments:
- '@logger.factory'
daftar.stock_api.yahoo_parser:
class: Drupal\daftar_stock_api\Service\YahooParserService
arguments:
- '@logger.factory'
daftar.stock_api.yahoo_api:
class: Drupal\daftar_stock_api\Service\YahooApiService
arguments:
- '@logger.factory'
@@ -0,0 +1,10 @@
<?php
namespace Drupal\daftar_stock_api\Exception;
use Exception;
class StockApiException extends Exception {
}
@@ -0,0 +1,93 @@
<?php
namespace Drupal\daftar_stock_api\Service;
use Drupal\Core\Logger\LoggerChannelFactory;
use Drupal\Core\Site\Settings;
use Drupal\daftar_stock_api\Exception\StockApiException;
use Drupal\daftar_stock_api\StockApiServiceInterface;
class AlphaVantageApiService implements StockApiServiceInterface {
const API_URL = 'https://www.alphavantage.co';
const API_PATH = 'query';
const SETTINGS_KEY_STOCK_API_KEY = 'daftar_stock_api_key';
/**
* Logger Factory.
*
* @var \Drupal\Core\Logger\LoggerChannelFactory
*/
protected $logger;
/**
* Constructor.
* @param LoggerChannelFactory $loggerFactory
*/
public function __construct(LoggerChannelFactory $loggerFactory) {
$this->logger = $loggerFactory->get('daftar_stock_api');
}
/**
* {@inheritdoc}
*/
public function doSymbolLookup($symbol) {
$jsonData = $this->doGet($symbol, 'OVERVIEW');
if (!isset($jsonData['Symbol']) || $jsonData['Symbol'] !== $symbol) {
return [];
}
return $jsonData;
}
/**
* @param $symbol
* @param $function
* @param array $params
* @return null
* @throws StockApiException
*/
private function doGet($symbol, $function, array $params = []) {
if (!$symbol) {
throw new StockApiException('Missing symbol');
}
if (!$function) {
$this->logger->error('Missing Stock API function.');
return NULL;
}
$apiKey = Settings::get(static::SETTINGS_KEY_STOCK_API_KEY, '');
if (!$apiKey) {
$this->logger->error('Missing Stock API Key.');
return NULL;
}
$client = new \GuzzleHttp\Client([
'base_uri' => static::API_URL,
'verify' => FALSE, // ignore invalid ssl
]);
$options = [
'query' => [
'apikey' => $apiKey,
'function' => strtoupper($function),
'symbol' => strtoupper($symbol),
],
];
$response = $client->get(static::API_PATH, $options);
if ($response->getStatusCode() != '200') {
$this->logger->error('Invalid response received from the Stock API.');
return NULL;
}
return json_decode($response->getBody(), TRUE);
}
}
@@ -0,0 +1,207 @@
<?php
namespace Drupal\daftar_stock_api\Service;
use DateTime;
use DOMDocument;
use Drupal\Core\Logger\LoggerChannelFactory;
use Drupal\daftar_stock_api\Exception\StockApiException;
use Drupal\daftar_stock_api\StockApiServiceInterface;
use Symfony\Component\HttpFoundation\Response;
class YahooApiService implements StockApiServiceInterface {
const API_URL = 'https://query2.finance.yahoo.com';
const API_PATH = '/v7/finance';
const API_ENDPOINT_OPTIONS = '/options';
/**
* Logger Factory.
*
* @var \Drupal\Core\Logger\LoggerChannelFactory
*/
protected $logger;
/**
* Constructor.
* @param LoggerChannelFactory $loggerFactory
*/
public function __construct(LoggerChannelFactory $loggerFactory) {
$this->logger = $loggerFactory->get('dsa_yahoo_api');
}
/**
* @param string $symbol
* @param string $date
*
* {@inheritdoc}
*/
public function doSymbolLookup($symbol) {
throw new StockApiException("doSymbolLookup has not been implemented.");
}
/**
* @param string $symbol
* @param string $date
* @param string $type
*
* @return array
* @throws \Drupal\daftar_stock_api\Exception\StockApiException
*/
public function getOptionContract(string $symbol, string $date, string $type, $strike): array {
$data = $this->getOptionContracts($symbol, $date, $type);
if ($data['error']) {
return $data;
}
$error = FALSE;
$message = '';
$needle = NULL;
foreach ($data['contents'] as $contract) {
if ($contract['strike'] == $strike) {
$needle = $contract;
break;
}
}
if (!$needle) {
$error = TRUE;
$message = 'Contract with given strike price is not included.';
}
return [
'error' => $error,
'message' => $message,
'contents' => ($error) ? NULL : $needle,
];
}
/**
* @param string $symbol
* @param string $date
* @param string $type
*
* @return array
* @throws \Drupal\daftar_stock_api\Exception\StockApiException
*/
public function getOptionContracts(string $symbol, string $date, string $type): array {
$data = $this->getOptionChain($symbol, $date);
if ($data['error']) {
return $data;
}
$error = FALSE;
$message = '';
if (empty($data['contents'][$type])) {
$error = TRUE;
$message = "Option for given type ${$type} are not included.";
}
return [
'error' => $error,
'message' => $message,
'contents' => ($error) ? NULL : $data['contents'][$type],
];
}
/**
* @param string $symbol
* @param string $date
*
* @return array
* @throws \Drupal\daftar_stock_api\Exception\StockApiException
* @throws \Exception
*/
public function getOptionChain(string $symbol, string $date): array {
$function = sprintf('%s/%s', self::API_ENDPOINT_OPTIONS, $symbol);
$dateTime = new DateTime($date);
$timestamp = $dateTime->format('U');
$data = $this->doGet($function, [
'date' => $timestamp,
]);
$error = FALSE;
$message = '';
if ($data['response_code'] !== Response::HTTP_OK) {
$error = TRUE;
$message = 'Response not received.';
}
elseif ($data['contents']['optionChain']['error']) {
$error = TRUE;
$message = 'Error in response received.';
}
elseif (empty($data['contents']['optionChain']['result'][0]['expirationDates'])
|| empty($data['contents']['optionChain']['result'][0]['strikes'])) {
$error = TRUE;
$message = 'Option chain not included.';
}
elseif (!$this->findValue($timestamp, $data['contents']['optionChain']['result'][0]['expirationDates'])) {
$error = TRUE;
$message = 'Option chain for given date is not included.';
}
return [
'error' => $error,
'message' => $message,
'contents' => ($error) ? NULL : $data['contents']['optionChain']['result'][0]['options'][0],
];
}
/**
* @param $needle
* @param $haystack
*
* @return bool
*/
private function findValue($needle, $haystack): bool {
$haystackIndex = array_flip($haystack);
return isset($haystackIndex[$needle]);
}
/**
* @param string $function
* @param array $params
*
* @return array An associative array containing status code and contents
* from the response.
* @throws \Drupal\daftar_stock_api\Exception\StockApiException
*/
private function doGet(string $function, array $params = []): array {
if (!$function) {
throw new StockApiException('Missing required function.');
}
if (substr($function, 0, 1) !== '/') {
throw new StockApiException("Function must begin with '/'.");
}
$client = new \GuzzleHttp\Client([
'base_uri' => static::API_URL,
'verify' => FALSE, // ignore invalid ssl
]);
$options = [
'allow_redirects' => FALSE,
'query' => $params,
];
$response = $client->get(self::API_PATH . $function, $options);
if ($response->getStatusCode() != Response::HTTP_OK) {
$this->logger->error('Invalid response received from Yahoo Finance API. Received status code: ' . $response->getStatusCode());
}
return [
'response_code' => $response->getStatusCode(),
'contents' => json_decode($response->getBody()->getContents(), TRUE),
];
}
}
@@ -0,0 +1,110 @@
<?php
namespace Drupal\daftar_stock_api\Service;
use DOMDocument;
use Drupal\Core\Logger\LoggerChannelFactory;
use Drupal\daftar_stock_api\Exception\StockApiException;
use Drupal\daftar_stock_api\StockApiServiceInterface;
class YahooParserService implements StockApiServiceInterface {
const API_URL = 'https://finance.yahoo.com';
const API_QUOTE_FUNCTION = 'quote';
/**
* Logger Factory.
*
* @var \Drupal\Core\Logger\LoggerChannelFactory
*/
protected $logger;
/**
* Constructor.
* @param LoggerChannelFactory $loggerFactory
*/
public function __construct(LoggerChannelFactory $loggerFactory) {
$this->logger = $loggerFactory->get('daftar_stock_api_yahoo');
}
/**
* {@inheritdoc}
*/
public function doSymbolLookup($symbol) {
throw new StockApiException("doSymbolLookup has not been implemented.");
}
/**
* @param $symbol
*
* @return string|null
*/
public function getPrice($symbol): ?string {
$htmlData = $this->doGet($symbol, static::API_QUOTE_FUNCTION . '/' . $symbol);
if (!$htmlData) {
return NUll;
}
$doc = new DOMDocument();
$doc->loadHTML($htmlData);
$quoteHeaderInfo = $doc->getElementById('quote-header-info');
if (!$quoteHeaderInfo) {
$this->logger->error('Unable to parse HTML response. Price not found. Symbol: ' . $symbol);
return NULL;
}
$finStreamers = $quoteHeaderInfo->getElementsByTagName('fin-streamer');
if ($finStreamers->count() <= 0) {
$this->logger->error('Missing element in HTML response.');
}
/** @var \DOMElement $elem */
foreach ($finStreamers as $elem) {
$dataField = $elem->getAttribute('data-field');
if ($dataField !== 'regularMarketPrice') {
continue;
}
return $elem->getAttribute('value');
}
$this->logger->error('Missing element with price in HTML response.');
return NULL;
}
/**
* @param $symbol
* @param $function
* @param array $params
* @return string|null
*/
private function doGet($symbol, $function, array $params = []) {
if (!$function) {
$this->logger->error('Missing required function.');
return NULL;
}
$client = new \GuzzleHttp\Client([
'base_uri' => static::API_URL,
'verify' => FALSE, // ignore invalid ssl
]);
$options = [
'allow_redirects' => FALSE,
'query' => [
'p' => strtoupper($symbol),
],
];
$response = $client->get($function, $options);
if ($response->getStatusCode() != '200') {
$this->logger->error('Invalid response received from Yahoo. Received status code: ' . $response->getStatusCode());
return NULL;
}
return $response->getBody()->getContents();
}
}
@@ -0,0 +1,19 @@
<?php
namespace Drupal\daftar_stock_api;
use Drupal\daftar_stock_api\Exception\StockApiException;
interface StockApiServiceInterface {
/**
* @param $symbol
* @return null
* @throws StockApiException
*/
public function doSymbolLookup($symbol);
}
@@ -0,0 +1,9 @@
name: Daftar Core
description: 'Defines core entities like account, portfolio and transaction (order, dividend and transfers).'
type: module
package: Daftar Content
core_version_requirement: ^8.8 || ^9
dependencies:
- drupal:user
- drupal:views
- entity:entity
@@ -0,0 +1,25 @@
<?php
namespace Drupal\daftar_core;
use Drupal\Core\Entity\EntityChangedInterface;
use Drupal\Core\Entity\ContentEntityInterface;
use Drupal\daftar_core\Entity\Helper\EntityCreatedInterface;
use Drupal\daftar_core\Entity\Helper\EntityKhatuInterface;
use Drupal\daftar_core\Entity\Helper\EntityReferencedInterface;
/**
* Provides an interface defining a chopda (portfolio) entity.
*
* @ingroup daftar
*/
interface ChopdaInterface extends
ContentEntityInterface,
EntityKhatuInterface,
EntityCreatedInterface,
EntityChangedInterface {
// no code
}
@@ -0,0 +1,66 @@
<?php
namespace Drupal\daftar_core;
use Drupal\Core\Entity\ContentEntityInterface;
use Drupal\Core\Entity\EntityChangedInterface;
use Drupal\Core\Entity\EntityChangedTrait;
use Drupal\Core\Entity\ContentEntityBase;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Field\BaseFieldDefinition;
use Drupal\daftar_core\Entity\Helper\EntityCreatedInterface;
use Drupal\daftar_core\Entity\Helper\EntityCreatedTrait;
/**
* Implements base class for all daftar entities.
*
* @ingroup daftar
*/
class DaftarBase extends ContentEntityBase implements
ContentEntityInterface,
EntityCreatedInterface,
EntityChangedInterface {
use EntityCreatedTrait;
use EntityChangedTrait;
/**
* {@inheritdoc}
*
* Define the field properties here.
*
* Field name, type and size determine the table structure.
*
* In addition, we can define how the field and its content can be manipulated
* in the GUI. The behaviour of the widgets used can be determined here.
* @throws \Drupal\Core\Entity\Exception\UnsupportedEntityTypeDefinitionException
*/
public static function baseFieldDefinitions(EntityTypeInterface $entity_type) {
$fields = parent::baseFieldDefinitions($entity_type);
if ($entity_type->hasKey('label')) {
$fields[$entity_type->getKey('label')] = BaseFieldDefinition::create('string')
->setLabel(t('Label'))
->setSetting('max_length', 100)
->setRequired(TRUE);
}
if ($entity_type->hasKey('created')) {
$fields[$entity_type->getKey('created')] = BaseFieldDefinition::create('created')
->setLabel(t('Created'))
->setDescription(t('The time that the entity was created.'))
->setRequired(TRUE);
}
if ($entity_type->hasKey('changed')) {
$fields[$entity_type->getKey('changed')] = BaseFieldDefinition::create('changed')
->setLabel(t('Changed'))
->setDescription(t('The time that the entity was last edited.'))
->setRequired(TRUE);
}
return $fields;
}
}
@@ -0,0 +1,35 @@
<?php
namespace Drupal\daftar_core;
use Drupal\Core\Entity\EntityChangedInterface;
use Drupal\Core\Entity\ContentEntityInterface;
use Drupal\daftar_core\Entity\Helper\EntityCreatedInterface;
use Drupal\daftar_core\Entity\Helper\EntityKhatuInterface;
/**
* Provides an interface defining a dividend entity.
*
* @ingroup daftar
*/
interface DividendInterface extends
ContentEntityInterface,
EntityKhatuInterface,
EntityCreatedInterface,
EntityChangedInterface {
public const DEFAULT_TYPE = 'cash';
public const DEFAULT_TAX_RATE = 'qualified';
public const TYPES = [
'cash' => 'Cash',
];
public const TAX_RATES = [
'qualified' => 'Qualified',
'ordinary' => 'Ordinary',
'other' => 'Other',
];
}
@@ -0,0 +1,78 @@
<?php
namespace Drupal\daftar_core\Entity;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\daftar_core\ChopdaInterface;
use Drupal\daftar_core\DaftarBase;
use Drupal\daftar_core\Entity\Helper\EntityKhatuTrait;
/**
* Defines the Chopda (Portfolio) entity.
*
* @ContentEntityType(
* id = "daftar_chopda",
* label = @Translation("Portfolio"),
* label_singular = @Translation("portfolio"),
* label_plural = @Translation("portfolios"),
* label_count = @PluralTranslation(
* singular = "@count portfolio",
* plural = "@count portfolios",
* ),
* base_table = "daftar_chopdao",
* entity_keys = {
* "id" = "cid",
* "uuid" = "uuid",
* "khatu" = "kid",
* "label" = "label",
* "created" = "created",
* "changed" = "changed",
* },
* handlers = {
* "views_data" = "Drupal\views\EntityViewsData"
* }
* )
*
* @ingroup daftar
*/
class Chopda extends DaftarBase implements ChopdaInterface {
use EntityKhatuTrait;
/**
* {@inheritdoc}
*
* Define the field properties here.
*
* Field name, type and size determine the table structure.
*
* In addition, we can define how the field and its content can be manipulated
* in the GUI. The behaviour of the widgets used can be determined here.
* @throws \Drupal\Core\Entity\Exception\UnsupportedEntityTypeDefinitionException
*/
public static function baseFieldDefinitions(EntityTypeInterface $entity_type) {
$fields = parent::baseFieldDefinitions($entity_type);
$fields += static::khatuBaseFieldDefinitions($entity_type);
$fields['cid']->setLabel(t('Portfolio ID'));
$fields['uuid']->setLabel(t('Portfolio UUID'));
return $fields;
}
/**
* {@inheritdoc}
*/
public function toArray() {
return[
'cid' => $this->id(),
'uuid' => $this->uuid(),
'kid' => $this->getKhatuId(),
'label' => $this->label(),
'created' => $this->getCreatedTime(),
'changed' => $this->getChangedTime(),
];
}
}
@@ -0,0 +1,32 @@
<?php
namespace Drupal\daftar_core\Entity\Helper;
/**
* Defines an interface for entity create timestamp tracking.
*
* @ingroup daftar
*/
interface EntityCreatedInterface {
/**
* Returns the time that the entity was created.
*
* @return int
* The timestamp of when the entity was created.
*/
public function getCreatedTime();
/**
* Sets the creation date of the entity.
*
* @param int $timestamp
* The timestamp of when the entity was created.
*
* @return $this
* The class instance that this method is called on.
*/
public function setCreatedTime($timestamp);
}
@@ -0,0 +1,37 @@
<?php
namespace Drupal\daftar_core\Entity\Helper;
/**
* Provides a trait for accessing created time.
*
* @see \Drupal\daftar_core\Entity\Helper\EntityCreatedInterface
* @ingroup daftar
*/
trait EntityCreatedTrait {
/**
* Returns the time that the entity was created.
*
* @return int
* The timestamp of when the entity was created.
*/
public function getCreatedTime() {
return $this->get('created')->value;
}
/**
* Sets the creation date of the entity.
*
* @param int $timestamp
* The timestamp of when the entity was created.
*
* @return $this
* The class instance that this method is called on.
*/
public function setCreatedTime($timestamp) {
$this->set('created', $timestamp);
return $this;
}
}
@@ -0,0 +1,56 @@
<?php
namespace Drupal\daftar_core\Entity\Helper;
use Drupal\daftar_core\KhatuInterface;
/**
* Defines a common interface for entities that belong to a khatu (account).
*
* @ingroup daftar
*/
interface EntityKhatuInterface {
public const KEY = 'khatu';
public const LABEL = 'Account ID';
public const TARGET_ENTITY_TYPE = 'daftar_khatu';
/**
* Returns the entity's khatu entity.
*
* @return \Drupal\daftar_core\KhatuInterface
* The khatu entity.
*/
public function getKhatu();
/**
* Sets the entity's khatu entity.
*
* @param \Drupal\daftar_core\KhatuInterface $khatu
* The khatu entity.
*
* @return $this
*/
public function setKhatu(KhatuInterface $khatu);
/**
* Returns the entity's khatu ID.
*
* @return int|null
* The khatu ID, or NULL in case the khatu ID field has not been set on
* the entity.
*/
public function getKhatuId();
/**
* Sets the entity's khatu entity ID.
*
* @param int $khatu_id
* The khatu entity ID.
*
* @return $this
*/
public function setKhatuId($khatu_id);
}
@@ -0,0 +1,87 @@
<?php
namespace Drupal\daftar_core\Entity\Helper;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Entity\Exception\UnsupportedEntityTypeDefinitionException;
use Drupal\Core\Field\BaseFieldDefinition;
use Drupal\daftar_core\KhatuInterface;
/**
* Provides a trait for entities that belong to a khatu (account).
*
* @see \Drupal\daftar_core\Entity\Helper\EntityKhatuInterface
* @ingroup daftar
*/
trait EntityKhatuTrait {
/**
* Returns an array of base field definitions for entity khatas (accounts).
*
* @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
* The entity type to add the reference field to.
*
* @return \Drupal\Core\Field\BaseFieldDefinition[]
* An array of base field definitions.
*
* @throws \Drupal\Core\Entity\Exception\UnsupportedEntityTypeDefinitionException
* Thrown when the entity type does not implement EntityKhatuInterface
* or if it does not have a "khatu" entity key.
*/
public static function khatuBaseFieldDefinitions(EntityTypeInterface $entity_type) {
if (!is_subclass_of($entity_type->getClass(), EntityKhatuInterface::class)) {
throw new UnsupportedEntityTypeDefinitionException(
sprintf('The entity type %s does not implement \Drupal\daftar\Entity\Helper\EntityReferencedInterface.', $entity_type->id())
);
}
if (!$entity_type->hasKey(EntityKhatuInterface::KEY)) {
throw new UnsupportedEntityTypeDefinitionException(
sprintf('The entity type %s does not have a "%s" entity key.', $entity_type->id(), EntityKhatuInterface::KEY)
);
}
return [
$entity_type->getKey(EntityKhatuInterface::KEY) => BaseFieldDefinition::create('entity_reference')
->setLabel(t(EntityKhatuInterface::LABEL))
->setDescription(t('The {daftar_khatao}.kid of the account.'))
->setSetting('target_type', EntityKhatuInterface::TARGET_ENTITY_TYPE)
->setRequired(TRUE),
];
}
/**
* {@inheritdoc}
*/
public function getKhatu() {
$key = $this->getEntityType()->getKey(EntityKhatuInterface::KEY);
return $this->get($key)->entity;
}
/**
* {@inheritdoc}
*/
public function setKhatu(KhatuInterface $entity) {
$key = $this->getEntityType()->getKey(EntityKhatuInterface::KEY);
$this->set($key, $entity);
return $this;
}
/**
* {@inheritdoc}
*/
public function getKhatuId() {
return $this->getEntityKey(EntityKhatuInterface::KEY);
}
/**
* {@inheritdoc}
*/
public function setKhatuId($khatu_id) {
$key = $this->getEntityType()->getKey(EntityKhatuInterface::KEY);
$this->set($key, $khatu_id);
return $this;
}
}
@@ -0,0 +1,53 @@
<?php
namespace Drupal\daftar_core\Entity\Helper;
use Drupal\Core\Entity\EntityInterface;
/**
* Defines an interface for entity create timestamp tracking.
*
* @ingroup daftar
*/
interface EntityReferencedInterface {
/**
* Returns the entity being referenced.
*
* @return \Drupal\Core\Entity\EntityInterface|null
* The referenced entity or NULL if nothing is referenced.
*/
public function getReference();
/**
* Sets the entity being referenced.
*
* @param \Drupal\Core\Entity\EntityInterface $entity
* The referenced entity.
*
* @return $this
*/
public function setReference(EntityInterface $entity);
/**
* Returns the referenced entity's ID.
*
* @return int|null
* The referenced entity ID, or NULL in case the reference field has not
* been set on the entity.
*/
public function getReferenceId();
/**
* Sets the referenced entity ID.
*
* @param int $entity_id
* The referenced entity ID.
*
* @return $this
*/
public function setReferenceId($entity_id);
}
@@ -0,0 +1,85 @@
<?php
namespace Drupal\daftar_core\Entity\Helper;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Entity\Exception\UnsupportedEntityTypeDefinitionException;
use Drupal\Core\Field\BaseFieldDefinition;
use Drupal\Core\StringTranslation\TranslatableMarkup;
/**
* Provides a trait for accessing referenced entity.
*
* @see \Drupal\daftar_core\Entity\Helper\EntityReferencedInterface
* @ingroup daftar
*/
trait EntityReferencedTrait {
/**
* Returns an array of base field definitions for entity references.
*
* @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
* The entity type to add the reference field to.
* @param string $target_type
* The entity type ID of the referenced entity.
*
* @return \Drupal\Core\Field\BaseFieldDefinition[]
* An array of base field definitions.
*
* @throws \Drupal\Core\Entity\Exception\UnsupportedEntityTypeDefinitionException
* Thrown when the entity type does not implement EntityReferencedInterface
* or if it does not have a "reference" entity key.
*/
public static function referenceBaseFieldDefinitions(EntityTypeInterface $entity_type, string $target_type) {
if (!is_subclass_of($entity_type->getClass(), EntityReferencedInterface::class)) {
throw new UnsupportedEntityTypeDefinitionException('The entity type ' . $entity_type->id() . ' does not implement \Drupal\daftar\Entity\Helper\EntityReferencedInterface.');
}
if (!$entity_type->hasKey('reference')) {
throw new UnsupportedEntityTypeDefinitionException('The entity type ' . $entity_type->id() . ' does not have a "reference" entity key.');
}
return [
$entity_type->getKey('reference') => BaseFieldDefinition::create('entity_reference')
->setLabel(new TranslatableMarkup('Reference ID'))
->setSetting('target_type', $target_type)
->setRequired(TRUE),
];
}
/**
* {@inheritdoc}
*/
public function getReference() {
$key = $this->getEntityType()->getKey('reference');
return $this->get($key)->entity;
}
/**
* {@inheritdoc}
*/
public function setReference(EntityInterface $entity) {
$key = $this->getEntityType()->getKey('reference');
$this->set($key, $entity);
return $this;
}
/**
* {@inheritdoc}
*/
public function getReferenceId() {
return $this->getEntityKey('reference');
}
/**
* {@inheritdoc}
*/
public function setReferenceId($entity_id) {
$key = $this->getEntityType()->getKey('reference');
$this->set($key, $entity_id);
return $this;
}
}
@@ -0,0 +1,99 @@
<?php
namespace Drupal\daftar_core\Entity;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\daftar_core\DaftarBase;
use Drupal\daftar_core\KhatuInterface;
use Drupal\user\EntityOwnerTrait;
/**
* Defines the Khatu (Account) entity.
*
* @ContentEntityType(
* id = "daftar_khatu",
* label = @Translation("Account"),
* label_singular = @Translation("account"),
* label_plural = @Translation("accounts"),
* label_count = @PluralTranslation(
* singular = "@count account",
* plural = "@count accounts",
* ),
* bundle_label = @Translation("Account Type"),*
* base_table = "daftar_khatao",
* entity_keys = {
* "id" = "kid",
* "uuid" = "uuid",
* "bundle" = "type",
* "owner" = "uid",
* "label" = "label",
* "created" = "created",
* "changed" = "changed",
* },
* handlers = {
* "views_data" = "Drupal\views\EntityViewsData"
* }
* )
*
* @ingroup daftar
*/
class Khatu extends DaftarBase implements KhatuInterface {
use EntityOwnerTrait;
/**
* {@inheritdoc}
*
* Define the field properties here.
*
* Field name, type and size determine the table structure.
*
* In addition, we can define how the field and its content can be manipulated
* in the GUI. The behaviour of the widgets used can be determined here.
* @throws \Drupal\Core\Entity\Exception\UnsupportedEntityTypeDefinitionException
*/
public static function baseFieldDefinitions(EntityTypeInterface $entity_type) {
$fields = parent::baseFieldDefinitions($entity_type);
$fields += static::ownerBaseFieldDefinitions($entity_type);
$fields['kid']->setLabel(t('Account ID'));
$fields['uuid']->setLabel(t('Account UUID'));
$fields['type']->setLabel(t('Account Type'));
$fields['uid']
->setDescription(t('The {user}.uid of the account owner.'));
return $fields;
}
/**
* {@inheritdoc}
*/
public function isBank() {
return $this->bundle() === 'bank';
}
/**
* {@inheritdoc}
*/
public function isBrokerage() {
return $this->bundle() === 'brokerage';
}
/**
* {@inheritdoc}
*/
public function toArray() {
return[
'kid' => $this->id(),
'uuid' => $this->uuid(),
'type' => $this->bundle(),
'uid' => $this->getOwnerId(),
'label' => $this->label(),
'created' => $this->getCreatedTime(),
'changed' => $this->getChangedTime(),
];
}
}
@@ -0,0 +1,87 @@
<?php
namespace Drupal\daftar_Core\Entity\Sodo;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Field\BaseFieldDefinition;
use Drupal\daftar_core\DividendInterface;
/**
* Defines the Dividend Transaction entity.
*
* @ContentEntityType(
* id = "daftar_dividend",
* label = @Translation("Dividend"),
* label_singular = @Translation("dividend"),
* label_plural = @Translation("dividends"),
* label_count = @PluralTranslation(
* singular = "@count dividend",
* plural = "@count dividends",
* ),
*
* base_table = "daftar_dividends",
*
* entity_keys = {
* "id" = "sid",
* "uuid" = "uuid",
* "khatu" = "kid",
* "bundle" = "type",
* "created" = "created",
* "changed" = "changed",
* },
* )
*
* @ingroup daftar
*/
class Dividend extends SodoEntityBase implements DividendInterface {
/**
* {@inheritdoc}
*
* Define the field properties here.
*
* Field name, type and size determine the table structure.
*
* In addition, we can define how the field and its content can be manipulated
* in the GUI. The behaviour of the widgets used can be determined here.
*/
public static function baseFieldDefinitions(EntityTypeInterface $entity_type) {
$fields = parent::baseFieldDefinitions($entity_type);
// Override parent label and description.
$fields['date']->setLabel(t('Date Paid'));
$fields['date']->setDescription(t('Date when this dividend was paid.'));
$fields['ex_date'] = BaseFieldDefinition::create('datetime')
->setLabel(t('Execution Date'))
->setDescription(t('Date when this dividend was executed.'))
->setSetting('datetime_type', 'date');
$fields['symbol'] = BaseFieldDefinition::create('string')
->setLabel(t('Symbol'))
->setSetting('max_length', 15)
->setRequired(TRUE);
$fields['quantity'] = BaseFieldDefinition::create('decimal')
->setLabel(t('Number of shares'))
->setDescription(t('Number of shares on which the dividend was paid out.'))
->setSetting('precision', 12)
->setSetting('scale', 4)
->setRequired(TRUE);
$fields['tax_rate'] = BaseFieldDefinition::create('string')
->setLabel(t('Tax Rate Classification'))
->setSetting('max_length', 15)
->setDescription(t('Tax rate classification of the dividend (e.g. qualified and ordinary).'));
$fields['auto_created'] = BaseFieldDefinition::create('boolean')
->setLabel(t('Auto Created'))
->setDescription(t('A boolean flag indicating whether the dividend was automatically added.'))
->setDefaultValue(FALSE);
return $fields;
}
}
@@ -0,0 +1,100 @@
<?php
namespace Drupal\daftar_core\Entity\Sodo;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Field\BaseFieldDefinition;
use Drupal\daftar_core\OrderInterface;
/**
* Defines the Order Transaction entity.
*
* @ContentEntityType(
* id = "daftar_order",
* label = @Translation("Order"),
* label_singular = @Translation("order"),
* label_plural = @Translation("orders"),
* label_count = @PluralTranslation(
* singular = "@count order",
* plural = "@count orders",
* ),
*
* base_table = "daftar_orders",
*
* entity_keys = {
* "id" = "sid",
* "uuid" = "uuid",
* "khatu" = "kid",
* "bundle" = "type",
* "created" = "created",
* "changed" = "changed",
* },
* )
*
* @ingroup daftar
*/
class Order extends SodoEntityBase implements OrderInterface {
/**
* {@inheritdoc}
*
* Define the field properties here.
*
* Field name, type and size determine the table structure.
*
* In addition, we can define how the field and its content can be manipulated
* in the GUI. The behaviour of the widgets used can be determined here.
*/
public static function baseFieldDefinitions(EntityTypeInterface $entity_type) {
$fields = parent::baseFieldDefinitions($entity_type);
$fields['action'] = BaseFieldDefinition::create('string')
->setLabel($entity_type->getBundleLabel())
->setRequired(TRUE)
->setReadOnly(TRUE);
$fields['symbol'] = BaseFieldDefinition::create('string')
->setLabel(t('Symbol'))
->setSetting('max_length', 15)
->setRequired(TRUE);
$fields['quantity'] = BaseFieldDefinition::create('decimal')
->setLabel(t('Quantity'))
->setDescription(t('Number of shares exchanged.'))
->setSetting('precision', 10)
->setSetting('scale', 4)
->setRequired(TRUE);
$fields['on_hand'] = BaseFieldDefinition::create('decimal')
->setLabel(t('Quantity On Hand'))
->setDescription(t('Number of shares currently on hand.'))
->setSetting('precision', 10)
->setSetting('scale', 4);
$fields['cps'] = BaseFieldDefinition::create('decimal')
->setLabel(t('Cost per Share'))
->setDescription(t('Price paid per share (computed).'))
->setSetting('precision', 10)
->setSetting('scale', 4)
->setComputed(TRUE);
$fields['commission'] = BaseFieldDefinition::create('decimal')
->setLabel(t('Commission'))
->setDescription(t('Commission charged by the brokerage.'))
->setSetting('precision', 10)
->setSetting('scale', 4)
->setDefaultValue(0);
$fields['fees'] = BaseFieldDefinition::create('decimal')
->setLabel(t('Fees'))
->setDescription(t('Fees charged for the transaction.'))
->setSetting('precision', 10)
->setSetting('scale', 4)
->setDefaultValue(0);
return $fields;
}
}
@@ -0,0 +1,70 @@
<?php
namespace Drupal\daftar_core\Entity\Sodo;
use Drupal\Core\Entity\ContentEntityInterface;
use Drupal\Core\Entity\EntityChangedInterface;
use Drupal\Core\Entity\EntityChangedTrait;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Field\BaseFieldDefinition;
use Drupal\daftar_core\DaftarBase;
use Drupal\daftar_core\Entity\Helper\EntityCreatedInterface;
use Drupal\daftar_core\Entity\Helper\EntityCreatedTrait;
use Drupal\daftar_core\Entity\Helper\EntityKhatuInterface;
use Drupal\daftar_core\Entity\Helper\EntityKhatuTrait;
/**
* Implements base class for sodo (transaction) entity types.
*
* @ingroup daftar
*/
abstract class SodoEntityBase extends DaftarBase implements
ContentEntityInterface,
EntityKhatuInterface,
EntityCreatedInterface,
EntityChangedInterface {
use EntityKhatuTrait;
use EntityCreatedTrait;
use EntityChangedTrait;
/**
* {@inheritdoc}
*
* Define the field properties here.
*
* Field name, type and size determine the table structure.
*
* In addition, we can define how the field and its content can be manipulated
* in the GUI. The behaviour of the widgets used can be determined here.
* @throws \Drupal\Core\Entity\Exception\UnsupportedEntityTypeDefinitionException
*/
public static function baseFieldDefinitions(EntityTypeInterface $entity_type) {
$fields = parent::baseFieldDefinitions($entity_type);
$fields += static::khatuBaseFieldDefinitions($entity_type);
// Fields attached to all transactions.
$fields['date'] = BaseFieldDefinition::create('datetime')
->setLabel(t('Transaction Date'))
->setDescription(t('Date when this transaction took place.'))
->setSetting('datetime_type', 'date')
->setRequired(TRUE);
$fields['amount'] = BaseFieldDefinition::create('decimal')
->setLabel(t('Amount'))
->setDescription(t('Total transaction amount.'))
->setSetting('precision', 12)
->setSetting('scale', 4)
->setRequired(TRUE);
$fields['notes'] = BaseFieldDefinition::create('string')
->setLabel(t('Notes'))
->setDescription(t('Short descriptive text for future references.'))
->setSetting('max_length', 255);
return $fields;
}
}
@@ -0,0 +1,59 @@
<?php
namespace Drupal\daftar_core\Entity\Sodo;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Field\BaseFieldDefinition;
use Drupal\daftar_core\TransferInterface;
/**
* Defines the Transfer Transaction entity.
*
* @ContentEntityType(
* id = "daftar_transfer",
* label = @Translation("Transfer"),
* label_singular = @Translation("transfer"),
* label_plural = @Translation("transfers"),
* label_count = @PluralTranslation(
* singular = "@count transfer",
* plural = "@count transfers",
* ),
*
* base_table = "daftar_transfers",
* entity_keys = {
* "id" = "id",
* "uuid" = "uuid",
* "khatu" = "kid",
* "bundle" = "type",
* "created" = "created",
* "changed" = "changed",
* },
* )
*
* @ingroup daftar
*/
class Transfer extends SodoEntityBase implements TransferInterface {
/**
* {@inheritdoc}
*
* Define the field properties here.
*
* Field name, type and size determine the table structure.
*
* In addition, we can define how the field and its content can be manipulated
* in the GUI. The behaviour of the widgets used can be determined here.
*/
public static function baseFieldDefinitions(EntityTypeInterface $entity_type) {
$fields = parent::baseFieldDefinitions($entity_type);
// Override parent label and description.
$fields['kid']->setDescription(t('The account from/to which the amount was transferred.'));
return $fields;
}
}
@@ -0,0 +1,49 @@
<?php
namespace Drupal\daftar_core;
use Drupal\Core\Entity\EntityChangedInterface;
use Drupal\Core\Entity\ContentEntityInterface;
use Drupal\daftar_core\Entity\Helper\EntityCreatedInterface;
use Drupal\user\EntityOwnerInterface;
/**
* Provides an interface defining a khatu (account) entity.
*
* @ingroup daftar
*/
interface KhatuInterface extends
ContentEntityInterface,
EntityOwnerInterface,
EntityCreatedInterface,
EntityChangedInterface {
/**
* Khatu (Account) entity types.
* Key/value pairs of machine names and labels.
*/
const TYPES = [
'bank' => 'Bank',
'brokerage' => 'Brokerage',
'ira_tradition' => 'Traditional IRA',
'ira_rollover' => 'Rollover IRA',
'ira_roth' => 'Roth IRA',
];
/**
* @return bool
* Returns TRUE if this entity is of bank type.
* FALSE otherwise.
*/
public function isBank();
/**
* @return bool
* Returns TRUE if this entity is of brokerage type.
* FALSE otherwise.
*/
public function isBrokerage();
}
@@ -0,0 +1,36 @@
<?php
namespace Drupal\daftar_core;
use Drupal\Core\Entity\EntityChangedInterface;
use Drupal\Core\Entity\ContentEntityInterface;
use Drupal\daftar_core\Entity\Helper\EntityCreatedInterface;
use Drupal\daftar_core\Entity\Helper\EntityKhatuInterface;
/**
* Provides an interface defining an order entity.
*
* @ingroup daftar
*/
interface OrderInterface extends
ContentEntityInterface,
EntityKhatuInterface,
EntityCreatedInterface,
EntityChangedInterface {
public const DEFAULT_TYPE = 'mkt';
public const TYPES = [
'mkt' => 'Market',
'lmt' => 'Limit',
'stp' => 'Stop',
'stplmt' => 'Stop Limit',
];
public const ACTIONS = [
'buy' => 'Buy',
'sell' => 'Sell',
];
}
@@ -0,0 +1,24 @@
<?php
namespace Drupal\daftar_core;
use Drupal\Core\Entity\EntityChangedInterface;
use Drupal\Core\Entity\ContentEntityInterface;
use Drupal\daftar_core\Entity\Helper\EntityCreatedInterface;
use Drupal\daftar_core\Entity\Helper\EntityKhatuInterface;
/**
* Provides an interface defining a transfer entity.
*
* @ingroup daftar
*/
interface TransferInterface extends
ContentEntityInterface,
EntityKhatuInterface,
EntityCreatedInterface,
EntityChangedInterface {
// no code
}
@@ -0,0 +1,10 @@
name: Dafter Content
description: Provides custom forms for adding content (e.g. Accounts, Orders, Dividend and Trasnsfers).
core: 8.x
type: module
package: Daftar
core_version_requirement: ^8 || ^9
version: 1.0.0
dependencies:
- drupal:node
- daftar_stock_api:daftar_stock_api
@@ -0,0 +1,16 @@
daftar_content.account_form:
path: '/portfolio/add/account/{type}'
defaults:
_form: '\Drupal\daftar_content\Form\AccountForm'
_title: 'Add an Account'
requirements:
_role: 'authenticated'
daftar_content.modal_form:
path: '/portfolio/modal/add/{type}'
defaults:
_title: 'Content Form Modal'
_controller: '\Drupal\daftar_content\Controller\ContentFormModalController::launchModal'
requirements:
_role: 'authenticated'
type: '[a-z]+'
@@ -0,0 +1,83 @@
<?php
namespace Drupal\daftar_content\Controller;
use Drupal\Core\Ajax\AjaxResponse;
use Drupal\Core\Ajax\OpenModalDialogCommand;
use Drupal\Core\Controller\ControllerBase;
use Drupal\Core\Form\FormBuilder;
use Drupal\daftar_content\Form\ContentBaseForm;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
/**
* Provides callback methods used for launching modals with content form.
* @package Drupal\daftar_content\Controller
*/
class ContentFormModalController extends ControllerBase {
/**
* @var FormBuilder
*/
protected $formBuilder;
/**
* OptionsController constructor.
* @param FormBuilder $formBuilder
*/
public function __construct(FormBuilder $formBuilder) {
$this->formBuilder = $formBuilder;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('form_builder')
);
}
/**
* Returns an AJAX response with the requested form.
* @param string $type
* @param int $width
* @return AjaxResponse
*/
public function launchModal($type, $width = 600) {
$form = self::getFormInfo($type);
if (NULL === $form) {
throw new NotFoundHttpException();
}
$response = new AjaxResponse();
$modalForm = $this->formBuilder->getForm($form['class'], $type, ContentBaseForm::DISPLAY_MODE_MODAL);
$response->addCommand(new OpenModalDialogCommand($form['title'], $modalForm, ['width' => $width]));
return $response;
}
/**
* Map id to name of a class that implements the form.
* @param string $type
* @return array|null
*/
private static function getFormInfo($type) {
switch($type) {
case 'brokerage':
return [
'class' => 'Drupal\daftar_content\Form\AccountForm',
'title' => 'Add a Brokerage Account',
];
case 'bank':
return [
'class' => 'Drupal\daftar_content\Form\AccountForm',
'title' => 'Add a Bank Account',
];
default:
return NULL;
}
}
}
@@ -0,0 +1,91 @@
<?php
namespace Drupal\daftar_content\Entity;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Field\BaseFieldDefinition;
/**
* Defines the Dividend Transaction entity.
*
* @ContentEntityType(
* id = "transaction_dividend",
* label = @Translation("Dividend"),
* base_table = "dc_transaction_dividend",
* entity_keys = {
* "id" = "id",
* "uuid" = "uuid",
* "account" = "aid",
* "changed" = "last_updated"
* },
* )
*
* @ingroup transaction_entity
*/
class Dividend extends TransactionEntityBase {
const TAX_TYPE_QUALIFIED = 10;
const TAX_TYPE_ORDINARY = 20;
/**
* {@inheritdoc}
*
* Define the field properties here.
*
* Field name, type and size determine the table structure.
*
* In addition, we can define how the field and its content can be manipulated
* in the GUI. The behaviour of the widgets used can be determined here.
*/
public static function baseFieldDefinitions(EntityTypeInterface $entity_type) {
$fields = parent::baseFieldDefinitions($entity_type);
// Override parent label and description.
$fields['transaction_date']->setLabel(t('Date Paid'));
$fields['transaction_date']->setDescription(t('Date when this dividend was paid.'));
$fields['execution_date'] = BaseFieldDefinition::create('datetime')
->setLabel(t('Execution Date'))
->setDescription(t('Date when this dividend was executed.'))
->setSetting('datetime_type', 'date');
$fields['qualified'] = BaseFieldDefinition::create('boolean')
->setLabel(t('Qualified dividend'))
->setDescription(t('A boolean flag indicating qualified dividend classification.'))
->setDefaultValue(TRUE);
$fields['auto_imported'] = BaseFieldDefinition::create('boolean')
->setLabel(t('Auto Imported'))
->setDescription(t('A boolean flag indicating whether the dividend was automatically imported.'))
->setDefaultValue(FALSE);
// Entity reference field, holds the reference to a taxonomy term object.
$fields['symbol'] = BaseFieldDefinition::create('entity_reference')
->setLabel(t('Symbol'))
->setDescription(t('Stock ticker.'))
->setSetting('target_type', 'taxonomy_term')
->setSetting('handler', 'default')
->setSetting('handler_settings', [
'target_bundles' => [
'symbols' => 'symbols',
],
])
->setReadOnly(TRUE)
->setRequired(TRUE);
$fields['quantity'] = BaseFieldDefinition::create('decimal')
->setLabel(t('Number of shares'))
->setDescription(t('Number of shares on which dividend was paid.'))
->setSetting('precision', 10)
->setSetting('scale', 2)
->setRequired(TRUE);
return $fields;
}
}
@@ -0,0 +1,58 @@
<?php
namespace Drupal\daftar_content\Entity\Interfaces;
use Drupal\node\NodeInterface;
/**
* Defines a common interface for entities that belong to an account.
*
* An account is a node entity to which all transaction entities are
* associated with.
*
* @ingroup transaction_entity
*/
interface TransactionEntityAccountInterface {
/**
* Returns the entity's account node entity.
*
* @return NodeInterface
* The account node entity.
*/
public function getAccount();
/**
* Sets the entity's account node entity.
*
* @param NodeInterface $account
* The account node entity.
*
* @return $this
*/
public function setAccount(NodeInterface $account);
/**
* Returns the entity's account node ID.
*
* @return int|null
* The account node ID, or NULL in case the account ID field has not been set on
* the entity.
*/
public function getAccountId();
/**
* Sets the entity's account node ID.
*
* @param int $aid
* The account node id.
*
* @return $this
*/
public function setAccountId($aid);
}
@@ -0,0 +1,19 @@
<?php
namespace Drupal\daftar_content\Entity\Interfaces;
use Drupal\Core\Entity\ContentEntityInterface;
use Drupal\Core\Entity\EntityChangedInterface;
/**
* Provides an interface defining a Transaction entity.
*
* We have this interface so we can join the other interfaces it extends.
*
* @ingroup transaction_entity
*/
interface TransactionEntityInterface extends ContentEntityInterface, TransactionEntityAccountInterface, EntityChangedInterface {
}
@@ -0,0 +1,173 @@
<?php
namespace Drupal\daftar_content\Entity;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Field\BaseFieldDefinition;
/**
* Defines the Order Transaction entity.
*
* @ContentEntityType(
* id = "transaction_order",
* label = @Translation("Order"),
* base_table = "dc_transaction_order",
* entity_keys = {
* "id" = "id",
* "uuid" = "uuid",
* "account" = "aid",
* "changed" = "last_updated",
* "notes" = "notes"
* },
* )
*
* @ingroup transaction_entity
*/
class Order extends TransactionEntityBase {
const ACTION_BUY = 10;
const ACTION_SELL = 20;
const ACTION_SELL_SHORT = 30;
const ACTION_BUY_TO_COVER = 40;
const TYPE_MARKET = 10;
const TYPE_LIMIT = 20;
const TYPE_STOP_MARKET = 30;
const TYPE_STOP_LIMIT = 40;
const TYPE_MARKET_ON_CLOSE = 50;
const TYPE_TRAILING_STOP_D = 60;
const TYPE_TRAILING_STOP_P = 70;
/**
* Maps order action to a constant.
*
* @param string $action Value to map.
*
* @return int Returns an integer value greater than zero if successfully mapped.
*/
public static function mapAction($action) {
switch(strtolower($action)) {
case 'buy':
return static::ACTION_BUY;
case 'sell':
return static::ACTION_SELL;
case 'sell short':
return static::ACTION_SELL_SHORT;
case 'buy to cover':
return static::ACTION_BUY_TO_COVER;
default:
return 0;
}
}
/**
* Maps order type to a constant.
*
* @param string $type Value to map
*
* @return int Returns an integer value greater than zero if successfully mapped.
*/
public static function mapType($type) {
switch(strtolower($type)) {
case 'market':
return static::TYPE_MARKET;
case 'limit':
return static::TYPE_LIMIT;
case 'stop market':
return static::TYPE_STOP_MARKET;
case 'stop limit':
return static::TYPE_STOP_LIMIT;
case 'market on close':
return static::TYPE_MARKET_ON_CLOSE;
case 'trailing stop $':
return static::TYPE_TRAILING_STOP_D;
case 'trailing stop %':
return static::TYPE_TRAILING_STOP_P;
default:
return 0;
}
}
/**
* {@inheritdoc}
*
* Define the field properties here.
*
* Field name, type and size determine the table structure.
*
* In addition, we can define how the field and its content can be manipulated
* in the GUI. The behaviour of the widgets used can be determined here.
*/
public static function baseFieldDefinitions(EntityTypeInterface $entity_type) {
$fields = parent::baseFieldDefinitions($entity_type);
$fields['action'] = BaseFieldDefinition::create('integer')
->setLabel(t('Order Action'))
->setDescription(t('The action being performed by this order.'))
->setSetting('size', 'tiny')
->setSetting('unsigned', TRUE)
->setRequired(TRUE);
$fields['type'] = BaseFieldDefinition::create('integer')
->setLabel(t('Order Type'))
->setDescription(t('The type of order being executed.'))
->setSetting('size', 'tiny')
->setSetting('unsigned', TRUE);
// Entity reference field, holds the reference to a taxonomy term object.
$fields['symbol'] = BaseFieldDefinition::create('entity_reference')
->setLabel(t('Symbol'))
->setDescription(t('Stock ticker.'))
->setSetting('target_type', 'taxonomy_term')
->setSetting('handler', 'default')
->setSetting('handler_settings', [
'target_bundles' => [
'symbols' => 'symbols',
],
])
->setReadOnly(TRUE)
->setRequired(TRUE);
$fields['quantity'] = BaseFieldDefinition::create('decimal')
->setLabel(t('Quantity'))
->setDescription(t('Number of shares exchanged.'))
->setSetting('precision', 10)
->setSetting('scale', 2)
->setRequired(TRUE);
$fields['on_hand'] = BaseFieldDefinition::create('decimal')
->setLabel(t('Quantity On Hand'))
->setDescription(t('Number of shares currently on hand.'))
->setSetting('precision', 10)
->setSetting('scale', 2);
$fields['pps'] = BaseFieldDefinition::create('decimal')
->setLabel(t('Price per Share'))
->setDescription(t('Price paid per share (computed).'))
->setSetting('precision', 10)
->setSetting('scale', 2)
->setComputed(TRUE);
$fields['commission'] = BaseFieldDefinition::create('decimal')
->setLabel(t('Commission'))
->setDescription(t('Commission charged by the brokerage.'))
->setSetting('precision', 10)
->setSetting('scale', 2)
->setDefaultValue(0);
$fields['fees'] = BaseFieldDefinition::create('decimal')
->setLabel(t('Fees'))
->setDescription(t('Fees charged for the transaction.'))
->setSetting('precision', 10)
->setSetting('scale', 2)
->setDefaultValue(0);
return $fields;
}
}
@@ -0,0 +1,108 @@
<?php
namespace Drupal\daftar_content\Entity\Traits;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Entity\Exception\UnsupportedEntityTypeDefinitionException;
use Drupal\Core\Field\BaseFieldDefinition;
use Drupal\Core\StringTranslation\TranslatableMarkup;
use Drupal\daftar_content\Entity\Interfaces\TransactionEntityAccountInterface;
use Drupal\node\NodeInterface;
trait TransactionAccountTrait {
/**
* Returns an array of base field definitions for entity account.
*
* @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
* The entity type to add the account field to.
*
* @return \Drupal\Core\Field\BaseFieldDefinition[]
* An array of base field definitions.
*
* @throws \Drupal\Core\Entity\Exception\UnsupportedEntityTypeDefinitionException
* Thrown when the entity type does not implement TransactionEntityAccountInterface or
* if it does not have an "account" entity key.
*/
public static function accountBaseFieldDefinitions(EntityTypeInterface $entity_type) {
if (!is_subclass_of($entity_type->getClass(), TransactionEntityAccountInterface::class)) {
throw new UnsupportedEntityTypeDefinitionException('The entity type ' . $entity_type->id() . ' does not implement \Drupal\daftar_content\Entity\Interfaces\TransactionEntityAccountInterface.');
}
if (!$entity_type->hasKey('account')) {
throw new UnsupportedEntityTypeDefinitionException('The entity type ' . $entity_type->id() . ' does not have an "account" entity key.');
}
return [
// Account field of the transaction.
// Entity reference field, holds the reference to a account node object.
$entity_type->getKey('account') => BaseFieldDefinition::create('entity_reference')
->setLabel(new TranslatableMarkup('Account ID'))
->setDescription(new TranslatableMarkup('The id of the account node this transaction is associated with.'))
->setSetting('target_type', 'node')
->setSetting('handler', 'default')
->setSetting('handler_settings', [
'target_bundles' => [
'account' => 'account',
],
])
->setReadOnly(TRUE),
];
}
/**
* Returns the entity's account node entity.
*
* @return \Drupal\node\NodeInterface
* The account node entity.
*/
public function getAccount() {
$key = $this->getEntityType()->getKey('account');
return $this->get($key)->entity;
}
/**
* Sets the entity's account node entity.
*
* @param \Drupal\node\NodeInterface $account
* The account node entity.
*
* @return $this
*/
public function setAccount(NodeInterface $account) {
$key = $this->getEntityType()->getKey('account');
$this->set($key, $account);
return $this;
}
/**
* Returns the entity's account node ID.
*
* @return int|null
* The account node ID, or NULL in case the account ID field has not been set on
* the entity.
*/
public function getAccountId() {
return $this->getEntityKey('account');
}
/**
* Sets the entity's account node ID.
*
* @param int $aid
* The account node id.
*
* @return $this
*/
public function setAccountId($aid) {
$key = $this->getEntityType()->getKey('account');
$this->set($key, $aid);
return $this;
}
}
@@ -0,0 +1,76 @@
<?php
namespace Drupal\daftar_content\Entity;
use Drupal\Core\Entity\ContentEntityBase;
use Drupal\Core\Entity\EntityChangedTrait;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Field\BaseFieldDefinition;
use Drupal\daftar_content\Entity\Interfaces\TransactionEntityInterface;
use Drupal\daftar_content\Entity\Traits\TransactionAccountTrait;
/**
* Implements Transaction Entity specific enhancements to the Content Entity Base class.
*
* @TODO Add transaction_id (for mapping to brokerage) and notes fields in
* a separate tables.
*
* @ingroup transaction_entity
*/
class TransactionEntityBase extends ContentEntityBase implements TransactionEntityInterface {
use EntityChangedTrait;
use TransactionAccountTrait;
/**
* {@inheritdoc}
*
* Define the field properties here.
*
* Field name, type and size determine the table structure.
*
* In addition, we can define how the field and its content can be manipulated
* in the GUI. The behaviour of the widgets used can be determined here.
* @throws \Drupal\Core\Entity\Exception\UnsupportedEntityTypeDefinitionException
*/
public static function baseFieldDefinitions(EntityTypeInterface $entity_type) {
$fields = parent::baseFieldDefinitions($entity_type);
$fields += static::accountBaseFieldDefinitions($entity_type);
$fields['transaction_date'] = BaseFieldDefinition::create('datetime')
->setLabel(t('Transaction Date'))
->setDescription(t('Date when this transaction took place.'))
->setSetting('datetime_type', 'date')
->setRequired(TRUE);
$fields['amount'] = BaseFieldDefinition::create('decimal')
->setLabel(t('Amount'))
->setDescription(t('Total transaction amount.'))
->setSetting('precision', 10)
->setSetting('scale', 2)
->setRequired(TRUE);
if ($entity_type->hasKey('notes')) {
$notesKey = $entity_type->getKey('notes');
$fields[$notesKey] = BaseFieldDefinition::create('string')
->setLabel(t('Notes'))
->setDescription(t('Short descriptive text for future references.'))
->setSetting('max_length', 255);
}
if ($entity_type->hasKey('changed')) {
$changedKey = $entity_type->getKey('changed');
$fields[$changedKey] = BaseFieldDefinition::create('changed')
->setLabel(t('Last Updated'))
->setDescription(t('The time that the transaction was last updated.'))
->setReadOnly(TRUE);
}
return $fields;
}
}
@@ -0,0 +1,70 @@
<?php
namespace Drupal\daftar_content\Entity;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Field\BaseFieldDefinition;
/**
* Defines the Transfer Transaction entity.
*
* @ContentEntityType(
* id = "transaction_transfer",
* label = @Translation("Transfer"),
* base_table = "dc_transaction_transfer",
* entity_keys = {
* "id" = "id",
* "uuid" = "uuid",
* "account" = "aid",
* "changed" = "last_updated",
* "notes" = "notes"
* },
* )
*
* @ingroup transaction_entity
*/
class Transfer extends TransactionEntityBase {
/**
* {@inheritdoc}
*
* Define the field properties here.
*
* Field name, type and size determine the table structure.
*
* In addition, we can define how the field and its content can be manipulated
* in the GUI. The behaviour of the widgets used can be determined here.
*/
public static function baseFieldDefinitions(EntityTypeInterface $entity_type) {
$fields = parent::baseFieldDefinitions($entity_type);
// Override parent label and description.
if ($entity_type->hasKey('account')) {
$key = $entity_type->getKey('account');
$fields[$key]->setLabel(t('To'));
$fields[$key]->setDescription(t('The Account to which the amount was deposited.'));
}
// Account field of the transaction.
// Entity reference field, holds the reference to a account node object.
$fields['account_withdrawn'] = BaseFieldDefinition::create('entity_reference')
->setLabel(t('From'))
->setDescription(t('The Account from which the amount was withdrawn.'))
->setSetting('target_type', 'node')
->setSetting('handler', 'default')
->setSetting('handler_settings', [
'target_bundles' => [
'account' => 'account',
],
])
->setRequired(TRUE)
->setReadOnly(TRUE);
return $fields;
}
}
@@ -0,0 +1,196 @@
<?php
namespace Drupal\daftar_content\Form;
use Drupal\Core\Ajax\AjaxResponse;
use Drupal\Core\Ajax\OpenModalDialogCommand;
use Drupal\Core\Entity\EntityStorageException;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Messenger\MessengerInterface;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
class AccountForm extends ContentBaseForm {
const MIN_LENGTH_ALIAS = 5;
const MAX_LENGTH_ALIAS = 30;
const MAX_LENGTH_NOTE = 150;
/**
* {@inheritdoc}
*/
public function getFormId() {
return 'daftar_content_brokerage_form';
}
/**
* @inheritDoc
*/
public function buildForm(array $form, FormStateInterface $form_state, $type = 'brokerage', $display_mode = self::DISPLAY_MODE_DEFAULT) {
$type = strtolower($type);
if($type !== 'brokerage' && $type != 'bank') {
throw new NotFoundHttpException();
}
$form_state->set('type', $type);
$fields = $this->getFieldDefinitions('account');
// Get anything we need from the base class.
$form = parent::buildForm($form, $form_state, $display_mode);
$form['title'] = [
'#type' => 'textfield',
'#title' => $fields['title']->getLabel(),
'#description' => $this->t('%title for the account. Do NOT enter your account number.', ['%title' => $fields['title']->getLabel()]),
'#maxlength' => self::MAX_LENGTH_ALIAS,
'#pattern' => '^[a-zA-Z0-9 ]{5,}$',
'#attributes' => ['title' => $this->t('Must be at least five characters long. Only alphanumeric and space characters are supported.')],
'#required' => TRUE,
];
switch($display_mode) {
case self::DISPLAY_MODE_EMBEDDED:
break;
case self::DISPLAY_MODE_DEFAULT:
default:
// set page title
$form['#title'] = ($type === 'brokerage')
? $this->t('Add a Brokerage Account')
: $this->t('Add a Bank Account');
// no break - intentionally left off
case self::DISPLAY_MODE_MODAL:
if (isset($fields['field_notes'])) {
$form['field_notes'] = [
'#type' => 'textarea',
'#title' => $fields['field_notes']->getLabel(),
'#description' => $this->t('Max characters allowed: %max', ['%max' => self::MAX_LENGTH_NOTE]),
'#maxlength' => self::MAX_LENGTH_NOTE,
'#rows' => 2,
'#resizable' => 'none',
];
}
}
return $form;
}
/**
* @inheritDoc
*/
public function validateForm(array &$form, FormStateInterface $form_state) {
$type = $form_state->get('type');
$title = trim($form_state->getValue('title'));
// @todo change to custom exception
if($type !== 'brokerage' && $type !== 'bank') {
throw new NotFoundHttpException();
}
$titleLabel = $form['title']['#title'];
if (strlen($title) < self::MIN_LENGTH_ALIAS) {
$form_state->setErrorByName('title', $this->t(
'%label must be at least %min characters long.',
['%label' => $titleLabel, '%min' => self::MIN_LENGTH_ALIAS]
));
}
elseif (preg_match('/[^a-zA-Z0-9 $]/', $title)) {
$form_state->setErrorByName('title', $this->t(
'%label contains invalid characters.',
['%label' => $titleLabel]
));
}
elseif ($this->isDuplicate($title)) {
$form_state->setErrorByName('title', $this->t(
'An account already exists under your portfolio with given alias.',
['%label' => $titleLabel]
));
}
}
/**
* @inheritDoc
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
$type = $form_state->get('type');
$title = trim($form_state->getValue('title'));
$notes = substr(trim($form_state->getValue('field_notes')), 0, self::MAX_LENGTH_NOTE);
$node = $this->nodeStorage->create([
'type' => 'account',
'status' => TRUE,
'uid' => $this->currentUser()->id(),
'title' => $title,
'field_account_status' => TRUE,
'field_account_type' => ($type == 'brokerage') ? 'br' : 'ba',
'field_notes' => $notes
]);
//@TODO improve for AJAX and embedded form.
// Remove message.
try {
$saveOperation = $node->save();
if ($saveOperation == SAVED_NEW) {
$this->messenger()->addMessage(
$this->t('Created %type account: %title', [
'%type' => $form_state->get('type'),
'%title' => $title,
]),
MessengerInterface::TYPE_STATUS
);
$form_state->set('account_node', $node);
return;
}
}
catch (EntityStorageException $e) {
$this->messenger()->addMessage(
$this->t('Unable to create account. Please try again.'),
MessengerInterface::TYPE_ERROR
);
}
$form_state->set('account_node', NULL);
}
/**
* @inheritDoc
*/
public function submitFormAjaxAdd(array &$form, FormStateInterface $form_state) {
$response = new AjaxResponse();
$response->addCommand(new OpenModalDialogCommand("Success!", 'The modal form has been submitted.'));
return $response;
}
protected function isDuplicate($title) {
$title = strtolower($title);
$isDuplicate = FALSE;
$results = $this->getAccountNids();
if (!empty($results)) {
$nodes = $this->nodeStorage->loadMultiple($results);
foreach ($nodes as $nid => $node) {
if ( $title === strtolower(trim($node->label())) ) {
$isDuplicate = TRUE;
break;
}
}
}
return $isDuplicate;
}
protected function getAccountNids() {
return $this->nodeStorage->getQuery()
->condition('status', 1)
->condition('type', 'account')
->condition('uid', $this->currentUser()->id())
->execute();
}
}
@@ -0,0 +1,114 @@
<?php
namespace Drupal\daftar_content\Form;
use Drupal\Core\Entity\EntityFieldManagerInterface;
use Drupal\Core\Form\FormBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\node\NodeStorageInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
abstract class ContentBaseForm extends FormBase {
const DISPLAY_MODE_DEFAULT = 'default';
const DISPLAY_MODE_MODAL = 'modal';
const DISPLAY_MODE_EMBEDDED = 'embedded';
/**
* @var EntityFieldManagerInterface
*/
protected $entityFieldManager;
/**
* @var NodeStorageInterface
*/
protected $nodeStorage;
public function __construct(EntityFieldManagerInterface $entity_field_manager, NodeStorageInterface $node_storage) {
$this->entityFieldManager = $entity_field_manager;
$this->nodeStorage = $node_storage;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('entity_field.manager'),
$container->get('entity_type.manager')->getStorage('node')
);
}
/**
* Callback for AJAX submit. Deriving class must implement this method.
*
* @param array $form
* An associative array containing the structure of the form.
* @param FormStateInterface $form_state
* The current state of the form.
* @return array
* Renderable array (the box element)
*/
public abstract function submitFormAjaxAdd(array &$form, FormStateInterface $form_state);
/**
* @inheritDoc
*/
public function buildForm(array $form, FormStateInterface $form_state, $display_mode = self::DISPLAY_MODE_DEFAULT) {
$form['actions'] = [
'#type' => 'actions',
'#weight' => 100,
];
switch($display_mode) {
case self::DISPLAY_MODE_EMBEDDED:
unset($form['actions']);
break;
case self::DISPLAY_MODE_MODAL:
$form['status_messages'] = [
'#type' => 'status_messages',
'#weight' => -100,
];
$form['actions']['submit'] = [
'#type' => 'submit',
'#value' => $this->t('Add'),
'#attributes' => [
'class' => [
'use-ajax',
],
],
'#ajax' => [
'callback' => [$this, 'submitFormAjaxAdd'],
'event' => 'click',
],
];
$form['#attached']['library'][] = 'core/drupal.dialog.ajax';
break;
case self::DISPLAY_MODE_DEFAULT:
default:
$form['actions']['submit'] = [
'#type' => 'submit',
'#value' => $this->t('Add'),
];
}
return $form;
}
/**
* @param string $node_bundle
* The bundle (e.g. account, order, dividend & etc).
* @return \Drupal\Core\Field\FieldDefinitionInterface[]
*/
protected function getFieldDefinitions($node_bundle) {
return $this->entityFieldManager->getFieldDefinitions('node', $node_bundle);
}
}
@@ -0,0 +1,10 @@
name: Dafter Import CSV
description: Allows user to import transactions from a CSV file.
core: 8.x
type: module
package: Daftar
core_version_requirement: ^8 || ^9
version: 1.0.0
dependencies:
- daftar_content:daftar_content
- daftar_stock_api:daftar_stock_api
@@ -0,0 +1,15 @@
daftar_import.csv_orders:
path: '/import/csv/orders'
defaults:
_form: '\Drupal\daftar_import\Form\ImportCsvOrdersForm'
_title: 'Import Orders from CSV'
requirements:
_role: 'authenticated'
daftar_import.csv_transfers:
path: '/import/csv/transfers'
defaults:
_form: '\Drupal\daftar_import\Form\ImportCsvTransfersForm'
_title: 'Import Transfers from CSV'
requirements:
_role: 'authenticated'
@@ -0,0 +1,10 @@
<?php
namespace Drupal\daftar_import\Exception;
use Exception;
class CsvDataException extends Exception {
}
@@ -0,0 +1,557 @@
<?php
namespace Drupal\daftar_import\Form;
use Drupal\Component\Utility\Bytes;
use Drupal\Component\Utility\Environment;
use Drupal\Core\Entity\EntityStorageException;
use Drupal\Core\Form\FormBase;
use Drupal\Core\Form\FormState;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Messenger\MessengerInterface;
use Drupal\daftar_content\Form\AccountForm;
use Drupal\daftar_content\Form\ContentBaseForm;
use Drupal\daftar_import\Exception\CsvDataException;
use Drupal\daftar_stock_api\Exception\StockApiException;
use Drupal\daftar_stock_api\StockApiServiceInterface;
use Drupal\file\FileInterface;
use Drupal\file\FileStorageInterface;
use Drupal\node\NodeInterface;
use Drupal\node\NodeStorageInterface;
use Drupal\taxonomy\TermStorageInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Provides shared methods.
* @package Drupal\daftar\Form
*/
abstract class ImportCsvBaseForm extends FormBase {
const MAX_CSV_FILE_SIZE = '5MB';
const TERM_TYPE_SYMBOLS = 'symbols';
const TERM_TYPE_EXCHANGES = 'exchanges';
const TERM_TYPE_SECTORS = 'sectors';
const TERM_TYPE_INDUSTRIES = 'industries';
/**
* @var NodeStorageInterface
*/
protected $nodeStorage;
/**
* @var TermStorageInterface
*/
protected $termStorage;
/**
* @var FileStorageInterface
*/
protected $fileStorage;
/**
* @var StockApiServiceInterface
*/
protected $stockApi;
/**
* @var AccountForm
*/
protected $accountForm;
public function __construct(NodeStorageInterface $node_storage, TermStorageInterface $term_storage, FileStorageInterface $file_storage, StockApiServiceInterface $stock_api) {
$this->nodeStorage = $node_storage;
$this->termStorage = $term_storage;
$this->fileStorage = $file_storage;
$this->stockApi = $stock_api;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
$form = new static(
$container->get('entity_type.manager')->getStorage('node'),
$container->get('entity_type.manager')->getStorage('taxonomy_term'),
$container->get('entity_type.manager')->getStorage('file'),
$container->get('daftar.stock_api.alphavantage')
);
$form->accountForm = AccountForm::create($container);
return $form;
}
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state) {
$brokerages = $this->getBrokerageOptions();
$maxCsvSize = intval($this->getMaxCsvSize());
$form['description'] = [
'#type' => 'item',
'#markup' => '<strong>Override or unset this element.</strong>',
'#weight' => '-20',
];
if (!empty($brokerages)) {
$form['brokerage'] = [
'#type' => 'select',
'#title' => $this->t('Brokerage Account'),
'#options' => $brokerages,
'#required' => TRUE,
'#validated' => TRUE, // Setting it to TRUE stops default validation. Must validate manually.
'#weight' => -15,
];
}
else {
$form['brokerage'] = [
'#type' => 'hidden',
'#value' => 'new'
];
}
$form += $this->buildAccountForm($form_state);
if (isset($form['title'])) {
unset($form['title']['#required']);
$form['title']['#title'] = $this->t('New Account Alias');
$form['title']['#weight'] = -10;
$form['title']['#states'] = [
'visible' => [
':input[name="brokerage"]' => ['value' => 'new'],
],
'required' => [
':input[name="brokerage"]' => ['value' => 'new'],
]
];
}
$form['csv_file'] = [
'#type' => 'managed_file',
'#title' => $this->t('CSV File'),
'#description' => $this->t(
'Upload a CSV file containing your transactions. Maximum file size allowed: %size',
['%size' => $this->getReadableFileSize($maxCsvSize)]
),
'#multiple' => FALSE,
'#upload_location' => 'private://imports',
'#upload_validators' => [
'file_validate_extensions' => ['csv'],
'file_validate_size' => [$maxCsvSize],
],
'#required' => TRUE,
];
$form['file_description'] = [
'#type' => 'item',
'#markup' => $this->t(
'Required columns in the CSV: %columns.',
['%columns' => $this->getNaturalLanguageJoin($this->getRequiredKeys())]
),
];
// Add a submit button that handles the submission of the form.
$form['actions']['submit'] = [
'#type' => 'submit',
'#value' => $this->t('Import'),
];
return $form;
}
protected function buildAccountForm(FormStateInterface $form_state) {
$accountFormState = new FormState();
$accountForm = $this->accountForm->buildForm([], $accountFormState, 'brokerage', ContentBaseForm::DISPLAY_MODE_EMBEDDED);
$form_state->set('account_form', $accountForm);
$form_state->set('account_form_state', $accountFormState);
return $accountForm;
}
/**
* {@inheritdoc}
*/
public function validateForm(array &$form, FormStateInterface $form_state) {
$brokerages = $this->getBrokerageOptions();
$brokerageSelected = $form_state->getValue('brokerage');
if ($brokerageSelected === 'new') {
$this->validateAccountForm($form_state);
}
// Make sure the selected brokerage account belongs to the current user.
elseif ($brokerageSelected !== 'new' && !isset($brokerages[$brokerageSelected])) {
$form_state->setErrorByName('brokerage', $this->t('Please select a brokerage account.'));
}
}
protected function validateAccountForm(FormStateInterface $form_state) {
/** @var FormStateInterface $accountFormState */
$newBrokerageTitle = trim($form_state->getValue('title'));
$accountForm = $form_state->get('account_form');
$accountFormState = $form_state->get('account_form_state');
$accountFormState->setValue('title', $newBrokerageTitle);
$accountFormState->setValue('type', 'brokerage');
$this->accountForm->validateForm($accountForm, $accountFormState);
$accountFormErrors = (array) $accountFormState->getErrors();
foreach ($accountFormErrors as $key => $error) {
$form_state->setErrorByName($key, $error);
}
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
/** @var NodeInterface $brokerage */
$brokerageSelected = $form_state->getValue('brokerage');
if ($brokerageSelected === 'new') {
$brokerage = $this->submitAccountForm($form_state);
}
else {
$brokerage = $this->getBrokerageNodeFromUuid($brokerageSelected);
}
if (NULL === $brokerage) {
$this->messenger()->addMessage(
$this->t('Unable to retrieve brokerage account.'),
MessengerInterface::TYPE_ERROR
);
return FALSE;
}
$csvFile = $form_state->getValue('csv_file');
if (isset($csvFile[0])) {
try {
$file = $this->preserveFile($csvFile[0]);
$data = $this->parseCsvToArray($file->getFileUri());
$this->messenger()->addMessage(
$this->t('Hooray! Passed Validation!'),
MessengerInterface::TYPE_STATUS
);
return [
'brokerage' => $brokerage,
'data' => $data,
];
}
catch (EntityStorageException $e) {
$this->messenger()->addMessage(
$this->t('Unable to process CSV file. Please try again.'),
MessengerInterface::TYPE_ERROR
);
}
catch (CsvDataException $e) {
$this->messenger()->addMessage(
['#type' => 'item', '#markup' => $e->getMessage()],
MessengerInterface::TYPE_ERROR
);
}
}
return FALSE;
}
protected function submitAccountForm(FormStateInterface $form_state) {
$accountForm = $form_state->get('account_form');
$accountFormState = $form_state->get('account_form_state');
$this->accountForm->submitForm($accountForm, $accountFormState);
return $accountFormState->get('account_node');
}
/**
* Remove whitespaces form a string and converts it to lowercase.
* @param string $key a string to sanitize
* @return string a sanitized string
*/
public static function sanitizeKey($key) {
$key = trim($key);
return strtolower($key);
}
/**
* @return array an array of required keys
*/
protected abstract function getRequiredKeys();
protected function getMaxCsvSize() {
static $max_size = -1;
if ($max_size < 0) {
// Start with site-wide max size.
$max_size = Environment::getUploadMaxSize();
// Reduce max_size if MAX_CSV_FILE_SIZE is less. Except if
// MAX_CSV_FILE_SIZE is zero, which indicates no limit.
$upload_max = Bytes::toInt(self::MAX_CSV_FILE_SIZE);
if ($upload_max > 0 && $upload_max < $max_size) {
$max_size = $upload_max;
}
}
return $max_size;
}
protected function getReadableFileSize($size, $precision = 2) {
$units = array('B','kB','MB','GB','TB','PB','EB','ZB','YB');
$step = 1024;
$i = 0;
while (($size / $step) > 0.9) {
$size = $size / $step;
$i++;
}
return round($size, $precision).$units[$i];
}
protected function getNaturalLanguageJoin(array $list, $conjunction = 'and') {
$last = array_pop($list);
if ($list) {
return implode(', ', $list) . ' ' . $conjunction . ' ' . $last;
}
return $last;
}
protected function getBrokerageOptions() {
$brokerages = [];
$results = $this->getBrokerageNids();
if (!empty($results)) {
$brokerages['new'] = '- ' . $this->t('Create a brokerage account') . ' -';
$nodes = $this->nodeStorage->loadMultiple($results);
foreach ($nodes as $nid => $node) {
$label = (boolean) $node->field_account_status->value
? $node->label()
: $node->label() . ' (inactive)';
$brokerages[$node->uuid()] = $label;
}
}
return $brokerages;
}
protected function getBrokerageNids() {
return $this->nodeStorage->getQuery()
->condition('status', 1)
->condition('type', 'account')
->condition('uid', $this->currentUser()->id())
->condition('field_account_type', 'br')
->execute();
}
/**
* @param string $uuid
* @return NodeInterface|null
*/
protected function getBrokerageNodeFromUuid($uuid) {
/** @var NodeInterface $brokerage */
$brokerage = NULL;
$results = $this->nodeStorage->getQuery()
->condition('status', 1)
->condition('type', 'account')
->condition('uuid', $uuid)
->condition('field_account_type', 'br')
->range(0, 1)
->execute();
if (!empty($results)) {
reset($results);
$brokerage = $this->nodeStorage->load(key($results));
}
return $brokerage;
}
/**
* @param $fid
* @return FileInterface|null
* @throws EntityStorageException
*/
protected function preserveFile($fid) {
/** @var FileInterface $file */
$file = $this->fileStorage->load($fid);
$file->setPermanent();
$file->save();
return $file;
}
/**
* @param $file_uri
* @return array an associative array of CSV rows indexed by header keys
* @throws EntityStorageException
* @throws CsvDataException
*/
protected function parseCsvToArray($file_uri) {
if(!file_exists($file_uri) || !is_readable($file_uri)) {
throw new EntityStorageException('File does not exists or is not readable.');
}
$header = NULL;
$rows = [];
if (($handle = fopen($file_uri, 'r')) !== FALSE ) {
while (($row = fgetcsv($handle, 1000)) !== FALSE) {
if (!$header){
// Sanitize keys in the header.
$header = array_map([__CLASS__, 'sanitizeKey'], $row);
// Check for missing keys.
$missingRequiredKeys = array_diff($this->getRequiredKeys(), $header);
if (!empty($missingRequiredKeys)) {
throw new CsvDataException($this->t(
'Missing required columns: %columns',
['%columns' => implode($missingRequiredKeys, ', ')]
));
}
continue;
}
$associativeRow = [];
foreach ($row as $i => $value) {
$associativeRow[$header[$i]] = $value;
}
$rows[] = $associativeRow;
}
fclose($handle);
}
return $rows;
}
/**
* Parses a date string and converts it to ISO8601 format.
* @param string $date_string A date string.
* @return string A date string in ISO8601 format.
* @throws CsvDataException
* @todo Maybe this function can be swapped out with a Drupal/PHP function.
*/
protected function getIso8601DateString($date_string) {
$parsedDate = date_parse($date_string);
if (FALSE === $parsedDate) {
throw new CsvDataException('Unable to parse date.');
}
if (!isset($parsedDate['year'])) {
throw new CsvDataException('Date is missing the year.');
}
$date = $parsedDate['year'];
if (isset($parsedDate['month'])) {
$date .= '-' . str_pad($parsedDate['month'], 2, '0', STR_PAD_LEFT);
}
if (isset($parsedDate['day'])) {
$date .= '-' . str_pad($parsedDate['day'], 2, '0', STR_PAD_LEFT);
}
return $date;
}
/**
* Find the taxonomy term ID of a given term.
*
* @param string $name The term name.
* @param string $vid The vocabulary ID.
* @param bool $create_term A boolean flag indicating whether to create
* a new term when not found.
*
* @return int|null Returns the term ID for a given term; null otherwise.
*
* @throws EntityStorageException
* @throws CsvDataException
* @throws StockApiException
*/
protected function getTermId($name, $vid, $create_term = FALSE) {
// Extract symbol from
if ($vid === static::TERM_TYPE_SYMBOLS) {
$symbol = strtoupper($name);
if (strpos($symbol, ':') !== FALSE) {
$symbolSplit = explode(':', $symbol);
if (!$symbolSplit[1]) {
throw new CsvDataException('Invalid symbol provided.');
}
$name = $symbolSplit[1];
}
}
$termIds = $this->termStorage->getQuery()
->condition('vid', $vid)
->condition('name', $name)
->range(0, 1)
->execute();
if (!empty($termIds)) {
return reset($termIds);
}
if ($create_term) {
return $this->createTerm($name, $vid);
}
return NULL;
}
/**
* Creates the taxonomy term entity for a given term.
*
* @param string $name The term name.
* @param string $vid The vocabulary ID.
*
* @return int|null Returns the term ID for a given term; null otherwise.
*
* @throws EntityStorageException
* @throws CsvDataException
* @throws StockApiException
*/
protected function createTerm($name, $vid) {
$data = [
'name' => $name,
'vid' => $vid,
];
if ($vid === static::TERM_TYPE_SYMBOLS) {
$profile = $this->stockApi->doSymbolLookup($name);
if (empty($profile)) {
return NULL;
}
$exchangeTid = $this->getTermId($profile['Exchange'], static::TERM_TYPE_EXCHANGES, TRUE);
$sectorTid = $this->getTermId($profile['Sector'], static::TERM_TYPE_SECTORS, TRUE);
$industryTid = $this->getTermId($profile['Industry'], static::TERM_TYPE_INDUSTRIES, TRUE);
$data += [
'field_company_name' => $profile['Name'],
'field_exchange' => ['target_id' => $exchangeTid],
'field_sectors' => ['target_id' => $sectorTid],
'field_industries' => ['target_id' => $industryTid],
];
}
$term = $this->termStorage->create($data);
$term->save();
return $term->id();
}
}
@@ -0,0 +1,130 @@
<?php
namespace Drupal\daftar_import\Form;
use Drupal\Core\Entity\EntityStorageException;
use Drupal\Core\Entity\EntityStorageInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\daftar_content\Entity\Order;
use Drupal\daftar_import\Exception\CsvDataException;
use Drupal\daftar_stock_api\Exception\StockApiException;
/**
* Implements form for processing order transactions from a CSV file.
* @package Drupal\daftar\Form
*/
class ImportCsvOrdersForm extends ImportCsvBaseForm {
/**
* @var EntityStorageInterface
*/
protected $orderStorage;
/**
* {@inheritdoc}
*/
public function getFormId() {
return 'daftar_csv_import_form';
}
/**
* @inheritDoc
*/
protected function getRequiredKeys() {
return [
'id',
'date',
'action',
'symbol',
'quantity',
'amount',
];
}
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state) {
$form = parent::buildForm($form, $form_state);
$form['#title'] = 'Import Orders from CSV';
$form['description']['#markup'] = $this->t('Import transactions such as Buy, Sell, Sell Short and Buy to Cover from your brokerage.');
return $form;
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
$data = parent::submitForm($form, $form_state);
// End processing if no CSV data was retrieved.
if (FALSE === $data) {
return;
}
$currentRow = 1; // Starts at 1 because first row is a header row.
try {
$this->orderStorage = \Drupal::entityTypeManager()->getStorage('transaction_order');
$brokerageNodeId = $data['brokerage']->id();
foreach ($data['data'] as $row) {
$currentRow++;
$this->createOrder($brokerageNodeId, $row);
}
}
catch(CsvDataException $e) {
$this->messenger->addError(
'Data error on row ' . $currentRow . ': ' . $e->getMessage()
);
}
catch (\Exception $e) {
// @TODO log to watchdog
$this->messenger()->addError(
"Import process stopped at row {$currentRow} due to internal errors."
);
}
}
/**
* Creates the order entity.
*
* @param int $aidBrokerage Node ID of the brokerage account.
* @param array $data An associative array containing CSV row.
*
* @throws CsvDataException
* @throws EntityStorageException
* @throws StockApiException
*/
protected function createOrder($aidBrokerage, array $data) {
$dateIso8601 = $this->getIso8601DateString($data['date']);
$symbolTid = $this->getTermId($data['symbol'], static::TERM_TYPE_SYMBOLS, TRUE);
$action = Order::mapAction($data['action']);
if (!$action) {
throw new CsvDataException('Unable to parse order action.');
}
$type = Order::mapType($data['type']);
if (!$type) {
throw new CsvDataException('Unable to parse order type.');
}
$this->orderStorage->create([
'aid' => $aidBrokerage,
'transaction_date' => $dateIso8601,
'symbol' => $symbolTid,
'action' => $action,
'type' => $type,
'amount' => $data['amount'],
'quantity' => $data['quantity'],
'on_hand' => $data['quantity'],
'commission' => (isset($data['commission']) && $data['commission'] !== NULL) ? $data['commission'] : 0,
'fees' => (isset($data['fees']) && $data['fees'] !== NULL) ? $data['fees'] : 0,
'notes' => (isset($data['notes'])) ? $data['notes'] : NULL,
])->save();
}
}
@@ -0,0 +1,188 @@
<?php
namespace Drupal\daftar_import\Form;
use Drupal\Core\Entity\EntityStorageException;
use Drupal\Core\Form\FormState;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Messenger\MessengerInterface;
use Drupal\daftar_import\Exception\CsvDataException;
/**
* Implements form for processing transfer transactions from a CSV file.
* @package Drupal\daftar\Form
*/
class ImportCsvTransfersForm extends ImportCsvBaseForm {
/**
* @var \Drupal\Core\Entity\EntityStorageInterface
*/
protected $transferStorage;
/**
* {@inheritdoc}
*/
public function getFormId() {
return 'daftar_import_csv_transfer_form';
}
/**
* @inheritDoc
*/
protected function getRequiredKeys() {
return [
'id',
'date',
'amount',
'action',
'account',
];
}
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state) {
$form = parent::buildForm($form, $form_state);
$form['#title'] = 'Import Transfers from CSV';
$form['description']['#markup'] = $this->t('Import transactions such as deposits and withdrawals.');
return $form;
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
$data = parent::submitForm($form, $form_state);
// End processing if no CSV data was retrieved.
if (FALSE === $data) {
return;
}
$currentRow = 1; // Starts at 1 because first row is a header row.
try {
$this->transferStorage = \Drupal::entityTypeManager()->getStorage('transaction_transfer');
$brokerageNodeId = $data['brokerage']->id();
foreach ($data['data'] as $row) {
$currentRow++;
$this->createTransfer($brokerageNodeId, $row);
}
}
catch (CsvDataException $e) {
$this->messenger->addError(
'Data error on row ' . $currentRow . ': ' . $e->getMessage()
);
}
catch (\Exception $e) {
// @TODO log to watchdog
$this->messenger()->addError(
"Import process stopped at row {$currentRow} due to internal errors."
);
}
}
/**
* Find the bank account node ID of a given account alias.
*
* @param string $alias Alias for the bank account.
* @param bool $create_account A boolean flag indicating whether to create
* a new bank account when not found.
*
* @return int|null Returns the bank account node ID for a given alias; null otherwise.
* @throws \Exception
*/
protected function getBankNodeId($alias, $create_account = FALSE) {
$nodeIds = $this->nodeStorage->getQuery()
->condition('status', 1)
->condition('type', 'account')
->condition('uid', $this->currentUser()->id())
->condition('title', $alias)
->condition('field_account_type', 'ba')
->range(0, 1)
->execute();
if (!empty($nodeIds)) {
return reset($nodeIds);
}
if ($create_account) {
return $this->createBankAccount($alias);
}
return NULL;
}
/**
* Creates a bank account for the user.
*
* @param string $alias Alias for the bank account.
*
* @return int|null Returns the bank account node ID when successfully created; null otherwise.
*
* @throws CsvDataException
*/
protected function createBankAccount($alias) {
$accountFormState = new FormState();
$accountForm = $this->accountForm->buildForm([], $accountFormState, 'bank');
$accountFormState->setValue('title', $alias);
$accountFormState->setValue('type', 'bank');
$this->accountForm->validateForm($accountForm, $accountFormState);
$accountFormErrors = (array) $accountFormState->getErrors();
if (!empty($accountFormErrors)) {
throw new CsvDataException("Unable to create bank account: " . reset($accountFormErrors));
}
$this->accountForm->submitForm($accountForm, $accountFormState);
$node = $accountFormState->get('account_node');
if ($node) {
return $node->id();
}
return NULL;
}
/**
* Creates the transfer entity.
*
* @param int $aidBrokerage Node ID of the brokerage account.
* @param array $data An associative array containing CSV row.
*
* @throws CsvDataException
* @throws EntityStorageException
* @throws \Exception
*/
protected function createTransfer($aidBrokerage, $data) {
$dateIso8601 = $this->getIso8601DateString($data['date']);
$bankNodeId = $this->getBankNodeId($data['account'], TRUE);
if (!$bankNodeId) {
throw new \Exception("Unable to create bank account for alias {$data['account']}.");
}
if (strtolower($data['action']) === 'deposit') {
$aid = $aidBrokerage;
$accountWithdrawn = $bankNodeId;
}
else {
$aid = $bankNodeId;
$accountWithdrawn = $aidBrokerage;
}
$this->transferStorage->create([
'aid' => $aid,
'account_withdrawn' => $accountWithdrawn,
'amount' => $data['amount'],
'transaction_date' => $dateIso8601,
])->save();
}
}
@@ -0,0 +1,9 @@
name: Dafter Quote
description: Provides quotes for a security, ETF or an option.
core: 8.x
type: module
package: Daftar
core_version_requirement: ^8 || ^9
version: 1.0.0
dependencies:
- daftar_stock_api:daftar_stock_api
@@ -0,0 +1,35 @@
daftar_quote.options:
path: '/quote/options'
defaults:
_title: 'Options Quote'
_controller: '\Drupal\daftar_quote\Controller\OptionsController::quote'
requirements:
_access: 'TRUE'
daftar_quote.v2.options.contracts:
path: '/v2/option/contracts/{symbol}/{type}/{expiration_date}'
defaults:
_title: 'Options Quote'
_controller: '\Drupal\daftar_quote\Controller\OptionsController::v2_contracts'
requirements:
_access: 'TRUE'
symbol: '[A-Z]{1,5}'
type: 'calls|puts'
expiration_date: '\d{4}-\d{2}-\d{2}'
daftar_quote.v2.options.contract:
path: '/v2/option/contracts/{contract_symbol}'
defaults:
_title: 'Option Contract'
_controller: '\Drupal\daftar_quote\Controller\OptionsController::v2_contract'
requirements:
_access: 'TRUE'
#contract_symbol: '[A-Z]{1,5}\d{6}(P|C)\d{8}'
daftar_quote.v2.options.parse_contract_symbol:
path: '/v2/option/contracts/parse'
defaults:
_title: 'Option Contract Parse'
_controller: '\Drupal\daftar_quote\Controller\OptionsController::v2_parse_contract_symbol'
requirements:
_access: 'TRUE'
@@ -0,0 +1,292 @@
<?php
namespace Drupal\daftar_quote\Controller;
use DateTime;
use Drupal\Core\Controller\ControllerBase;
use Drupal\Core\Site\Settings;
use Drupal\daftar_stock_api\Exception\StockApiException;
use Drupal\daftar_stock_api\Service\YahooApiService;
use Drupal\daftar_stock_api\Service\YahooParserService;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
/**
* Provides callback methods used for launching modals with content form.
* @package Drupal\daftar_content\Controller
*/
class OptionsController extends ControllerBase {
const SETTINGS_KEY_OPTIONS_TOKEN = 'daftar_quote_options_token';
const CONTRACT_SYMBOL_PATTERN = '/^([A-Z]{1,5})(\d{6})([PC])(\d{8})$/';
const CONTRACT_SYMBOL_FD_PATTERN = '/^-?([A-Z]{1,5})(\d{6})([PC])(\d{1,4}([.]\d{1,2})?)$/';
// Tran Date Contact Symbol QT PPS Total
const CONTRACT_SYMBOL_FD_PATTERN2 = '/^(\d{2}\/\d{2}\/\d{4})[ ]+YOU (SOLD|BOUGHT) (OPENING|CLOSING) TRANSACTION[ ]+-?([A-Z]{1,5})(\d{6})([PC])(\d{1,4}[\.]?\d{0,2})[ ]+(-?\d{1,4})[ ]+\$(\d{1,4}[\.]?\d{0,4})[ ]+\$(-?\d+[,]?\d{0,3}[\.]?\d{0,2})[ ]+\$(\d{1,3}[\.]?\d{0,2})[ ]+\$(\d{1,3}[\.]?\d{0,2}).*$/';
// Tran Date Contact Symbol QT PPS Total
const CONTRACT_SYMBOL_FD_PATTERN2B = '/^(\d{2}\/\d{2}\/\d{4})YOU (SOLD|BOUGHT) (OPENING|CLOSING) TRANSACTION-?([A-Z]{1,5})(\d{6})([PC])(\d{1,4}[\.]?\d{0,2})(-?\d{1,4})\$(\d{1,4}[\.]?\d{0,4})\$(-?\d+[,]?\d{0,3}[\.]?\d{0,2})\$(\d{1,3}[\.]?\d{0,2})\$(\d{1,3}[\.]?\d{0,2}).*$/';
const CONTRACT_SYMBOL_TDA_PATTERN = '/^(Sold|Bought) (\d+) ([A-Z]{1,5}) ([A-Z][a-z]{2} \d{1,2} \d{4}) (\d{1,4}([.]\d{1,2})?) (Call|Put) @ (\d{1,4}([.]\d{1,2})?)$/';
/**
* @var \Drupal\daftar_stock_api\Service\YahooParserService
*/
protected YahooParserService $yahooParser;
/**
* @var \Drupal\daftar_stock_api\Service\YahooApiService
*/
protected YahooApiService $yahooApi;
/**
* OptionsController constructor.
*
* @param YahooParserService $yahoo_parser_service
* @param \Drupal\daftar_stock_api\Service\YahooApiService $yahoo_api_service
*/
public function __construct(YahooParserService $yahoo_parser_service, YahooApiService $yahoo_api_service) {
$this->yahooParser = $yahoo_parser_service;
$this->yahooApi = $yahoo_api_service;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container): OptionsController {
return new static(
$container->get('daftar.stock_api.yahoo_parser'),
$container->get('daftar.stock_api.yahoo_api')
);
}
/**
* @param string $symbol
* @param string $type
* @param string $expiration_date
* @param Request $request
*
* @return JsonResponse
*/
public function v2_contracts(string $symbol, string $type, string $expiration_date, Request $request): JsonResponse {
$tokenCheck = $this->checkToken($request);
if ($tokenCheck) {
return $tokenCheck;
}
$strike = $request->get('strike');
return $this->v2_fetchContracts($symbol, $type, $expiration_date, $strike);
}
/**
* @param string $contract_symbol
* @param Request $request
*
* @return JsonResponse
*/
public function v2_contract(string $contract_symbol, Request $request): JsonResponse {
$tokenCheck = $this->checkToken($request);
if ($tokenCheck) {
return $tokenCheck;
}
if (preg_match(self::CONTRACT_SYMBOL_PATTERN, $contract_symbol, $matches)) {
$date = DateTime::createFromFormat('ymd', $matches[2]);
$type = ($matches[3] === 'P') ? 'puts' : 'calls';
$strike = intval($matches[4])/1000;
return $this->v2_fetchContracts($matches[1], $type, $date->format('Y-m-d'), $strike);
}
return new JsonResponse([
'error' => TRUE,
'message' => $this->t('Invalid contract symbol.'),
], Response::HTTP_BAD_REQUEST);
}
/**
* @param string $contract_symbol
* @param Request $request
*
* @return JsonResponse
*/
public function v2_parse_contract_symbol(Request $request): JsonResponse {
$tokenCheck = $this->checkToken($request);
if ($tokenCheck) {
return $tokenCheck;
}
$parsed = [];
$contractSymbol = $request->request->get('contract_symbol');
//$contractSymbol = $request->get('contract_symbol');
if (preg_match(self::CONTRACT_SYMBOL_PATTERN, $contractSymbol, $matches)) {
$date = DateTime::createFromFormat('ymd', $matches[2]);
$parsed = [
'symbol' => $matches[1],
'expiration_date' => $date->format('m/d/Y'),
'type' => ($matches[3] === 'P') ? 'PUT' : 'CALL',
'strike' => intval($matches[4])/1000,
'account' => '',
];
}
elseif (preg_match(self::CONTRACT_SYMBOL_FD_PATTERN, $contractSymbol, $matches)) {
$date = DateTime::createFromFormat('ymd', $matches[2]);
$parsed = [
'symbol' => $matches[1],
'expiration_date' => $date->format('m/d/Y'),
'type' => ($matches[3] === 'P') ? 'PUT' : 'CALL',
'strike' => $matches[4],
'account' => 'FD',
];
}
elseif (preg_match(self::CONTRACT_SYMBOL_FD_PATTERN2, $contractSymbol, $matches)
|| preg_match(self::CONTRACT_SYMBOL_FD_PATTERN2B, $contractSymbol, $matches)) {
$date = DateTime::createFromFormat('ymd', $matches[5]);
$action = substr($matches[2], 0, 1) . '2';
$action .= ($matches[3] === 'CLOSING') ? 'X' : 'O';
$parsed = [
'tran_date' => $matches[1],
'action' => $action,
'symbol' => $matches[4],
'expiration_date' => $date->format('m/d/Y'),
'type' => ($matches[6] === 'P') ? 'PUT' : 'CALL',
'strike' => $matches[7],
'quantity' => $matches[8],
'account' => 'FD2',
'price' => $matches[9],
'comm' => $matches[11],
'fees' => $matches[12]
];
}
elseif (preg_match(self::CONTRACT_SYMBOL_TDA_PATTERN, $contractSymbol, $matches)) {
$date = DateTime::createFromFormat('M d Y', $matches[4]);
$parsed = [
'symbol' => $matches[3],
'expiration_date' => $date->format('m/d/Y'),
'type' => ($matches[7] === 'Put') ? 'PUT' : 'CALL',
'strike' => $matches[5],
'account' => 'TDA',
'action' => ($matches[1] === 'Sold') ? 'S2O' : 'B2O',
'quantity' => $matches[2],
'price' => $matches[8],
];
}
if ($parsed) {
$parsed['contract'] = NULL;
try {
$type = ($parsed['type'] == 'CALL') ? 'calls' : 'puts';
$contractData = $this->yahooApi->getOptionContract(
$parsed['symbol'], $parsed['expiration_date'], $type, $parsed['strike']
);
if (!$contractData['error']) {
$parsed['contract'] = $contractData['contents'];
}
} catch (StockApiException $e) {
// do nothing
}
return new JsonResponse([
'error' => FALSE,
'message' => '',
'contents' => $parsed,
], Response::HTTP_OK);
}
else {
return new JsonResponse([
'error' => TRUE,
'message' => $this->t('Invalid or missing contract symbol.'),
'contract_symbol' => $contractSymbol,
], Response::HTTP_BAD_REQUEST);
}
}
/**
* @param Request $request
* @return JsonResponse
*/
public function quote(Request $request): JsonResponse {
$tokenCheck = $this->checkToken($request);
if ($tokenCheck) {
return $tokenCheck;
}
$symbol = $request->get('symbol');
if (!$symbol) {
return new JsonResponse([
'error' => TRUE,
'message' => $this->t('Missing symbol.'),
], Response::HTTP_BAD_REQUEST);
}
$price = $this->yahooParser->getPrice($symbol);
if ($price) {
return new JsonResponse([
'error' => FALSE,
'message' => 'success',
'price' => $price,
], Response::HTTP_OK);
}
return new JsonResponse([
'error' => TRUE,
'message' => $this->t('Unable to parse price.'),
], Response::HTTP_INTERNAL_SERVER_ERROR);
}
/**
* @param string $symbol
* @param string $type
* @param string $expiration_date
* @param $strike
*
* @return \Symfony\Component\HttpFoundation\JsonResponse
*/
private function v2_fetchContracts(string $symbol, string $type, string $expiration_date, $strike = ""): JsonResponse {
if ($type == 'CALL') {
$type == 'calls';
}
if ($type == 'PUT') {
$type == 'puts';
}
try {
if ($strike) {
$data = $this->yahooApi->getOptionContract($symbol, $expiration_date, $type, $strike);
}
else {
$data = $this->yahooApi->getOptionContracts($symbol, $expiration_date, $type);
}
return new JsonResponse($data, Response::HTTP_OK);
}
catch (StockApiException $e) {
//do nothing
}
return new JsonResponse([
'error' => TRUE,
'message' => $this->t('Error occurred while finding contract.'),
], Response::HTTP_INTERNAL_SERVER_ERROR);
}
private function checkToken(Request $request): ?JsonResponse {
$optionsToken = Settings::get(static::SETTINGS_KEY_OPTIONS_TOKEN, '');
if (!$optionsToken) {
return new JsonResponse([
'error' => TRUE,
'message' => $this->t('Token setup has not been completed.'),
], Response::HTTP_INTERNAL_SERVER_ERROR);
}
$receivedToken = $request->get('token');
if ($receivedToken !== $optionsToken) {
return new JsonResponse([
'error' => TRUE,
'message' => $this->t('Invalid token.'),
], Response::HTTP_FORBIDDEN);
}
return NULL;
}
}