Skip to content

Instantly share code, notes, and snippets.

@nsilva
Created June 20, 2024 09:14
Show Gist options
  • Select an option

  • Save nsilva/2e55b7a4e6a41eb67cf1d8f970c608bb to your computer and use it in GitHub Desktop.

Select an option

Save nsilva/2e55b7a4e6a41eb67cf1d8f970c608bb to your computer and use it in GitHub Desktop.
<?php
namespace App\Domains\Altix\Services;
use App\Domains\Altix\Models\Document;
use App\Domains\Altix\Models\DocumentTemplate;
use App\Domains\Altix\Models\Enums\DocumentTemplateType as DocumentTemplateTypeEnum;
use App\Domains\Altix\Models\Enums\DocumentTypeId;
use App\Domains\Altix\Models\Fund;
use App\Domains\Altix\Models\Investment;
use App\Domains\Altix\Models\UserFund;
use App\Domains\Altix\Models\UserFundDocument;
use App\Domains\Altix\Models\UserSignedDocument;
use App\Domains\Altix\Services\Traits\CanDownloadPdfFile;
use App\Domains\Auth\Models\User;
use App\Exceptions\Codes\MysqlErrorCodes;
use App\Services\BaseService;
use Carbon\Carbon;
use Illuminate\Database\Connection;
use Illuminate\Database\Eloquent\Builder as EloquentBuilder;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use Illuminate\Database\Eloquent\Relations\Relation;
use Illuminate\Database\QueryException;
use Illuminate\Support\Collection as SupportCollection;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
use Symfony\Component\HttpFoundation\StreamedResponse;
class FundService extends BaseService
{
use CanDownloadPdfFile;
protected DocumentService|null $documentService = null;
protected DocumentTemplateService|null $documentTemplateService = null;
protected InvestmentService|null $investmentService = null;
/**
* FundService constructor.
*
* @param Fund $fund
*/
public function __construct(Fund $fund)
{
$this->model = $fund;
}
/**
* Check whether the user can invest the supplied amount in the supplied fund.
*
* @param User $user
* @param Fund|string $fund Fund instance or fund UUID.
* @param int $amount
* @return string|null Error message, if there is a problem with the amount to invest.
*/
public function checkFundInvestmentAmountByUser(User $user, Fund|string $fund, int $amount): ?string
{
if (! ($fund instanceof Fund)) {
$fund = $this->getByUuid($fund, ['id', 'fund_size', 'min_ticket', 'amount_committed']);
}
$userAlreadyInvestedIntoFund = $user->hasAlreadyInvestedInFund($fund->id);
return $this->checkFundInvestmentAmountAlreadyInvested($fund, $amount, $userAlreadyInvestedIntoFund);
}
/**
* Check the amount to invest into a fund in which the user possibly already invested.
* Check for:
* - Maximum amount
* - Room in the fund, if enabled
* - If the user already has invested into the fund, the minimum increase amount
* - If the user did not yet invest into the fund, the maximum of the minimum amount to invest and the minimum ticket for the fund
*
* @param Fund $fund
* @param int $amount
* @param bool|null $userAlreadyInvestedIntoFund
* @return string|null Error message, if there is a problem with the amount to invest.
*/
public function checkFundInvestmentAmountAlreadyInvested(Fund $fund, int $amount, bool $userAlreadyInvestedIntoFund = null): ?string
{
$message = null;
// check for maximum amount of investment
$maximumAmount = config('altix.investment.amount.maximum');
if ($amount > $maximumAmount) {
$message = __('The maximum amount to invest is :maximum.', [
'maximum' => numberWithCurrency($maximumAmount),
]);
}
// check for room within fund, if enabled
if (config('altix.investment.amount.check-room')) {
$maximumAmount = $fund->getRoomAmount();
if ($amount > $maximumAmount) {
$message = __('The maximum amount to invest in this fund is :maximum.', [
'maximum' => numberWithCurrency($maximumAmount),
]);
}
}
// if the user already invested in the fund the amount should be at least the minimum increase amount
if ($userAlreadyInvestedIntoFund) {
$minimumIncreaseAmount = config('altix.investment.amount.minimum-increase');
if ($amount < $minimumIncreaseAmount) {
$message = __('The minimum amount to invest in this fund is :minimum.', [
'minimum' => numberWithCurrency($minimumIncreaseAmount),
]);
}
} else {
// if the user has not yet invested in the fund the amount should be at least the maximum of the minimum ticket and the minimum amount
$minimumAmount = config('altix.investment.amount.minimum');
$minimumTicket = $fund->min_ticket;
if ($minimumTicket > $minimumAmount) {
if ($amount < $minimumTicket) {
$message = __('The minimum amount to invest in this fund is :minimum.', [
'minimum' => numberWithCurrency($minimumTicket),
]);
}
} elseif ($amount < $minimumAmount) {
$message = __('The minimum amount to invest is :minimum.', [
'minimum' => numberWithCurrency($minimumAmount),
]);
}
}
return $message;
}
/**
* Register click from user to view more information on fund.
*
* @param User $user
* @param Fund $fund
* @return void
*/
public function clickMore(User $user, Fund $fund): void
{
UserFund::query()->updateOrCreate([
'user_id' => $user->id,
'fund_id' => $fund->id,
], [
'clicked_detail_at' => now(),
]);
}
/**
* Retrieve fund by its slug.
*
* @param string $slug
* @param string[]|array|string $columns
* @param bool $noException
* @return Fund|null
*/
public function getBySlug(string $slug, array|string $columns = ['*'], bool $noException = false): ?Fund
{
$query = $this->getEnabledBuilder()
->where('slug', $slug);
if ($noException) {
return $query->first($columns);
}
return $query->firstOrFail($columns);
}
/**
* Retrieve the latest fund document by the slug of the document type.
*
* @param Fund $fund
* @param string $slug
* @param array|string|string[] $columns
* @return Document|null
*/
public function getDocumentByTypeSlug(Fund $fund, string $slug, array|string $columns = ['*']): ?Document
{
$document = $fund->documents()
->joinRelationship('documentType')
->select($columns)
->where('document_types.slug', $slug)
->orderBy('documents.id', 'desc')
->limit(1)
->first();
if (null === $document) {
$message = sprintf('Last document of type by slug \'%s\' for fund #%u: not found', $slug, $fund->id);
Log::error($message);
throw (new ModelNotFoundException())->setModel(Document::class);
}
return $document;
}
/**
* Get fund document download response for user.
* @param User $user
* @param Fund $fund
* @param Document $document
* @param bool $inline
* @return StreamedResponse|null
*/
public function getDownloadDocumentResponse(User $user, Fund $fund, Document $document, bool $inline = false): ?StreamedResponse
{
return DB::transaction(function (Connection $connection) use ($user, $fund, $document, $inline) {
$this->registerDocumentDownload($user, $fund, $document);
[$path, $filename] = $this->getDocumentService()->getPathAndFilename($document);
$response = $this->getDownloadFileResponse($path, $filename, $inline, 'encrypted');
if (null === $response) {
$connection->rollBack();
}
return $response;
});
}
/**
* Retrieve the downloaded documents of a fund, indexed by document ID.
*
* @param User $user
* @param int $fundId
* @return UserFundDocument[]|Collection
*/
public function getDownloadedDocuments(User $user, int $fundId): Collection
{
return $user->userFundDocuments($fundId)
->with('documentType')
->get()
->keyBy('document_id');
}
/**
* Retrieve the fund documents.
*
* @TODO Handle multiple versions of document types.
*
* @param int $fundId
* @return Document[]|Collection
*/
public function getFundDocuments(int $fundId): Collection
{
return Document::with('documentType')
->select('documents.*')
->where(
[
'model_type' => Fund::class,
'model_id' => $fundId,
]
)
->join('document_types', 'document_types.id', '=', 'documents.document_type_id')
->orderBy('document_types.order')
->get();
}
/**
* Retrieve the fund documents.
*
* @TODO Handle multiple versions of document types.
*
* @param User $user
* @param int $fundId
* @return Document[]|Collection
*/
public function getFundDocumentsForUser(User $user, int $fundId): Collection
{
return Document::with('documentType')
->join('document_types', 'document_types.id', '=', 'documents.document_type_id')
->leftJoin('user_fund_documents', function ($join) use ($fundId, $user) {
$join->on('user_fund_documents.document_id', '=', 'documents.id')
->where('user_fund_documents.fund_id', $fundId)
->where('user_fund_documents.user_id', $user->id);
})
->distinct()
->select('documents.*')
->addSelect('document_types.order')
->addSelect('user_fund_documents.downloaded_at')
->where(
[
'documents.model_type' => Fund::class,
'documents.model_id' => $fundId,
]
)
->whereNotIn('documents.document_type_id', [
DocumentTypeId::INVESTOR_AGREEMENT,
DocumentTypeId::OTHER,
])
->orderBy('document_types.order')
->get();
}
/**
* Retrieve the fund documents with the corresponding documents to sign by the user.
*
* @param User $user
* @param int $fundId
* @param int|null $investmentId
* @return Document[]|Collection
*/
public function getFundDocumentsWithUserSignedDocuments(User $user, int $fundId): Collection
{
// retrieve fund documents
$documents = $this->getFundDocumentsForUser($user, $fundId);
// retrieve investments of the user into fund
$investmentIds = $this->getUserFundInvestments($user, $fundId);
// retrieve documents to sign by user that the user actually signed
$userSignedDocuments = $this->getUserSignedDocumentsForInvestments($user, $fundId, $investmentIds);
$userSignedDocuments->each(function (UserSignedDocument $userSignedDocument) {
$documentType = $userSignedDocument->documentType ?? $userSignedDocument->documentTemplateType?->getDocumentType();
$userSignedDocument->document_type_id = $documentType?->id;
$userSignedDocument->setRelation('document_type', $documentType);
});
$documents->each(function (Document $document) use ($userSignedDocuments) {
$documentTypeId = $document->document_type_id;
$documentUserSignedDocuments = $userSignedDocuments->filter(function (UserSignedDocument $userSignedDocument) use ($document, $documentTypeId) {
return $userSignedDocument->document_id === $document->id || $userSignedDocument->document_type_id === $documentTypeId;
});
$document->setRelation('userSignedDocuments', $documentUserSignedDocuments);
});
return $documents;
}
/**
* Retrieve the fund documents by type.
*
* @TODO Handle multiple versions of document types.
*
* @param int $fundId
* @param array $documentTypes
* @return Document[]|Collection
*/
public function getFundDocumentsByType(int $fundId, array $documentTypes): Collection
{
return Document::query()
->select('documents.*')
->where(
[
'model_type' => Fund::class,
'model_id' => $fundId,
]
)
->join('document_types', 'document_types.id', '=', 'documents.document_type_id')
->whereIn('document_types.id', $documentTypes)
->orderBy('document_types.order')
->get();
}
/**
* Retrieve the document templates of a fund for a specific investment.
*
* @param int $fundId
* @param Investment $investment
* @param DocumentTemplateTypeEnum[]|array $documentTemplateTypes
* @param array|string $columns
* @return SupportCollection
*/
public function getFundDocumentTemplatesByTypeForInvestment(int $fundId, Investment $investment, array $documentTemplateTypes, array|string $columns = ['*']): SupportCollection
{
/** @var DocumentTemplate[]|Collection $documentTemplates */
$documentTemplates = collect();
foreach ($documentTemplateTypes as $documentTemplateType) {
$documentTemplates[$documentTemplateType->value] = $this->getDocumentTemplateService()->getLastDocumentTemplateByType($documentTemplateType, $fundId, $investment->company_type_id, $columns);
}
return $documentTemplates;
}
/**
* Retrieve the number of funds for which the user has pending actions.
* That is, funds for which the user has pending:
* - signing the fund NDA to unlock the fund documentation
* - investment for a fund
*
* @param User $user
* @return int
*/
public function getPendingFundsCount(User $user): int
{
return $this->getPendingFunds($user, 'id')->count();
}
/**
* Retrieve funds for which the user has pending actions.
* That is, funds for which the user has pending:
* - signing the fund NDA to unlock the fund documentation
* - investment for a fund
*
* @param User $user
* @param array|string $columns
* @return Fund[]|Collection
*/
public function getPendingFunds(User $user, array|string $columns = ['*']): Collection
{
$columns = $this->buildModelColumns($columns);
// fund documents pending to sign
/** @var Fund[]|Collection $funds */
$funds = $user->userSignedDocuments()
->inProgress()
->joinRelationship('fund')
->select($columns)
->distinct()
->get();
$fundIds = $funds->pluck('id');
$investmentFunds = $user->investments()
->pendingUser()
->joinRelationship('fund')
->distinct()
->select($columns)
->whereNotIn('funds.id', $fundIds)
->get();
return $funds->concat($investmentFunds);
}
/**
* Retrieve the user fund record, creating it if necessary.
*
* @param int $userId
* @param int $fundId
* @return UserFund
*/
public function getOrCreateUserFund(int $userId, int $fundId): UserFund
{
try {
/** @var UserFund $userFund */
$userFund = UserFund::query()->firstOrCreate(
[
'user_id' => $userId,
'fund_id' => $fundId,
]
);
return $userFund;
} catch (QueryException $e) {
// if the record was created after checking whether it existed return that records
if (MysqlErrorCodes::DUPLICATE_ENTRY === $e->getCode()) {
return $this->getUserFundById($userId, $fundId);
}
throw $e;
}
}
/**
* Retrieve user fund record.
*
* @param User $user
* @param int $fundId
* @return UserFund|null
*/
public function getUserFund(User $user, int $fundId): ?UserFund
{
return $user->userFund($fundId)
->first();
}
/**
* Retrieve user fund record by ID's.
*
* @param int $userId
* @param int $fundId
* @return UserFund|null
*/
public function getUserFundById(int $userId, int $fundId): ?UserFund
{
/** @var UserFund|null $userFund */
$userFund = UserFund::query()->where([
'user_id' => $userId,
'fund_id' => $fundId,
])
->first();
return $userFund;
}
/**
* Retrieve the fund documents with the corresponding documents to sign by the user.
*
* @param User $user
* @return Fund[]|Collection
*/
public function getUserFundsWithDocuments(User $user): Collection
{
$funds = $user->funds()
->orderBy('funds.name')
->get(['funds.id', 'funds.uuid', 'funds.slug', 'funds.name']);
$funds->each(function (Fund $fund) use ($user) {
$documents = $this->getFundDocumentsWithUserSignedDocuments($user, $fund->id);
$fund->setRelation('documents', $documents);
$userFund = $this->getUserFund($user, $fund->id);
$fund->setRelation('userFund', $userFund);
// retrieve investment contracts for fund
$investmentService = $this->getInvestmentService();
$documentTemplates = collect();
$fund->investments = $investmentService->getUserFundInvestments($user, $fund->id)->sortBy('id');
foreach ($fund->investments as $investment) {
$documentTemplates[] = $investmentService->getInvestmentContractWithUserSignedDocuments($investment);
}
$documentTemplates->sortBy('id');
$fund->setRelation('documentTemplates', $documentTemplates);
});
return $funds;
}
/**
* Determine whether the user has investments as a company into fund.
*
* @param User $user
* @param int $fundId
* @return bool
*/
public function hasUserFundInvestmentsAsCompany(User $user, int $fundId): bool
{
$count = $this->getUserFundInvestmentsAsCompanyBuilder($user, $fundId)
->count();
return 0 < $count;
}
/**
* Increase the amount committed in a fund.
*
* @param Fund $fund
* @param int $amount
* @return bool
*/
public function increaseAmountCommitted(Fund $fund, int $amount): bool
{
return DB::transaction(function () use ($amount, $fund) {
$fund = Fund::query()->lockForUpdate()->find($fund->id);
if (! $fund->hasRoomForAmount($amount)) {
return false;
}
return $fund->update([
'amount_committed' => DB::raw("amount_committed + {$amount}"),
]);
});
}
/**
* Register the download of a fund document by a user.
*
* @pre NB: It is assumed that this function is called within a transaction.
*
* @param User $user
* @param Fund $fund
* @param Document $document
* @return void
*/
public function registerDocumentDownload(User $user, Fund $fund, Document $document): void
{
$wheres = [
'user_id' => $user->id,
'fund_id' => $fund->id,
'document_id' => $document->id,
];
$data = [
'downloaded_at' => now(),
];
$userFundDocument = UserFundDocument::query()->lockForUpdate()->where($wheres)->first();
if (null === $userFundDocument) {
// try to create it
try {
$userFundDocument = UserFundDocument::create($wheres + $data);
} catch (QueryException $e) {
if (MysqlErrorCodes::DUPLICATE_ENTRY !== $e->getCode()) {
throw $e;
}
// create failed, so now it does exists, so retrieve it, again
$userFundDocument = UserFundDocument::query()->lockForUpdate()->where($wheres)->first();
}
}
// record already exists
if (null === $userFundDocument->download_at) {
$userFundDocument->update($data);
}
// determine whether all information and contracts have been seen
$this->updateUserFundSeen($user, $fund->id);
}
/**
* Register user having seen the fund contracts.
*
* @param int $userId
* @param int $fundId
* @return void
*/
public function registerFundContractsSeen(int $userId, int $fundId): void
{
$this->updateUserFund($userId, $fundId, 'seen_contracts_at', now());
}
/**
* Register user having seen the fund information.
*
* @param int $userId
* @param int $fundId
* @return void
*/
public function registerFundInformationSeen(int $userId, int $fundId): void
{
$this->updateUserFund($userId, $fundId, 'seen_information_at', now());
}
/**
* Register user signing fund NDA in user fund table.
*
* @param int $userId
* @param int $fundId
* @param Carbon $signedAt
* @return void
*/
public function registerNdaSign(int $userId, int $fundId, Carbon $signedAt): void
{
$this->updateUserFund($userId, $fundId, 'nda_signed_at', $signedAt);
}
/**
* Retrieve the investments into fund by user.
*
* @param User $user
* @param int $fundId
* @return EloquentBuilder[]|Collection
*/
protected function getUserFundInvestments(User $user, int $fundId, array|string $columns = ['id']): array|Collection
{
return $this->getUserFundInvestmentsBuilder($user, $fundId)
->get($columns);
}
/**
* Retrieve documents to sign by user that the user actually signed for specific investments.
*
* @param User $user
* @param int $fundId
* @param Collection|array $investmentIds
* @param DocumentTypeId[]|int[]|array|null $documentTypeIds
* @return Collection
*
* @TODO Move to UserSignedDocumentService
*/
public function getUserSignedDocumentsForInvestments(User $user, int $fundId, Collection|array $investmentIds, array $documentTypeIds = null): Collection
{
$query = UserSignedDocument::query()
->with('document')
->with('documentTemplate:id,document_template_type_id')
->with('documentTemplateType:document_template_types.id,document_template_types.name')
->where([
'user_signed_documents.user_id' => $user->id,
])
->where(function ($query) use ($fundId, $investmentIds) {
$query->orWhere('user_signed_documents.fund_id', $fundId)
->orWhereIn('user_signed_documents.investment_id', $investmentIds);
})
->where(function ($query) {
$query->orWhereNotNull('user_signed_documents.signed_at')
->orWhereNotNull('user_signed_documents.path_signed_altix');
})
->orderBy('user_signed_documents.id');
if (null !== $documentTypeIds) {
$documentTemplateTypeEmums = [];
foreach ($documentTypeIds as $documentTypeId) {
$documentTemplateTypeEmums[] = DocumentTemplateTypeEnum::getFromDocumentTypeId($documentTypeId);
}
$query->leftJoinRelationship('documentTemplate.documentTemplateType')
->leftJoinRelationship('document');
$query->where(function ($query) use ($documentTypeIds, $documentTemplateTypeEmums) {
$query->orWhereIn('documents.document_type_id', $documentTypeIds)
->orWhereIn('document_template_types.name', $documentTemplateTypeEmums);
});
}
/** @var UserSignedDocument[]|Collection $userSignedDocuments */
$userSignedDocuments = $query->select([
'user_signed_documents.id',
'user_signed_documents.uuid',
'user_signed_documents.user_id',
'user_signed_documents.document_id',
'user_signed_documents.document_template_id',
'user_signed_documents.fund_id',
'user_signed_documents.investment_id',
'user_signed_documents.name',
'user_signed_documents.provider_name',
'user_signed_documents.status',
'user_signed_documents.signed_at',
'user_signed_documents.signed_altix_at',
'user_signed_documents.path_signed',
'user_signed_documents.path_signed_altix',
'user_signed_documents.created_at',
'user_signed_documents.updated_at',
])
->get();
return $userSignedDocuments;
}
/**
* Retrieve the investments as a company into fund by user.
*
* @param User $user
* @param int $fundId
* @return EloquentBuilder[]|Collection
*/
protected function getUserFundInvestmentsAsCompany(User $user, int $fundId, array|string $columns = ['*']): array|Collection
{
return $this->getUserFundInvestmentsBuilder($user, $fundId)
->where('as_company', 1)
->get($columns);
}
/**
* Retrieve query builder for investments as a company into fund by user.
*
* @param User $user
* @param int $fundId
* @return EloquentBuilder|Relation
*/
protected function getUserFundInvestmentsAsCompanyBuilder(User $user, int $fundId): EloquentBuilder|Relation
{
return $this->getUserFundInvestmentsBuilder($user, $fundId)
->where('as_company', 1);
}
/**
* Retrieve query builder for investments into fund by user.
*
* @param User $user
* @param int $fundId
* @return EloquentBuilder|Relation
*/
protected function getUserFundInvestmentsBuilder(User $user, int $fundId): EloquentBuilder|Relation
{
return $user->investments()
->where([
'fund_id' => $fundId,
]);
}
/**
* Update a single attribute of the user fund record, possibly overwriting an existing value.
*
* @param int $userId
* @param int $fundId
* @param string $attribute
* @param mixed $value
* @return UserFund
*/
protected function updateUserFund(int $userId, int $fundId, string $attribute, mixed $value = null, bool $overwrite = false): UserFund
{
$userFund = $this->getOrCreateUserFund($userId, $fundId);
$where = [
'id' => $userFund->id,
];
if (! $overwrite) {
if (null !== $userFund->{$attribute}) {
return $userFund;
}
$where[$attribute] = null;
}
UserFund::query()
->where($where)
->update([$attribute => $value]);
return $userFund->refresh();
}
/**
* Determine whether all information and contracts of a fund have been seen by a user.
*
* @param User $user
* @param int $fundId
* @return void
*/
protected function updateUserFundSeen(User $user, int $fundId): void
{
// create or retrieve user fund record
$userFund = $this->getOrCreateUserFund($user->id, $fundId);
// return if already registered everything
if (null !== $userFund->seen_information_at && null !== $userFund->seen_contracts_at) {
return;
}
// lock for update to ensure correctness of the seen dates
$userFund = UserFund::lockForUpdate()->find($userFund->id);
$documents = $this->getFundDocuments($fundId);
$userFundDocuments = $this->getDownloadedDocuments($user, $fundId);
if (! $userFundDocuments->isEmpty()) {
// assume all documents have been seen
$seenContracts = true;
$seenInformation = true;
foreach ($documents as $document) {
// ignore fund NDA
$documentType = $document->documentType;
if (DocumentTypeId::NON_DISCLOSURE_AGREEMENT->value === $documentType->id) {
continue;
}
// if the document has not been downloaded, set corresponding variable to false
$userFundDocument = $userFundDocuments[$document->id] ?? null;
$isDownloaded = null !== $userFundDocument?->downloaded_at;
// if some document has not been seen, update the corresponding variable
if (! $isDownloaded) {
if ($documentType->require_unlock) {
$seenContracts = false;
} else {
$seenInformation = false;
}
// return if neither have been seen
if (! $seenContracts && ! $seenInformation) {
return;
}
}
}
// update seen times, if still unset
if ($seenContracts || $seenInformation) {
$data = [];
$now = now();
if ($seenInformation && null === $userFund->seen_information_at) {
$data['seen_information_at'] = $now;
}
if ($seenContracts && null === $userFund->seen_contracts_at) {
$data['seen_contracts_at'] = $now;
}
if (empty($data)) {
return;
}
$userFund->update($data);
}
}
}
/**
* Retrieve DocumentService instance.
*
* @return DocumentService
*/
protected function getDocumentService(): DocumentService
{
if (null === $this->documentService) {
$this->documentService = resolve(DocumentService::class);
}
return $this->documentService;
}
/**
* Retrieve DocumentTemplateService instance.
*
* @return DocumentTemplateService
*/
protected function getDocumentTemplateService(): DocumentTemplateService
{
if (null === $this->documentTemplateService) {
$this->documentTemplateService = resolve(DocumentTemplateService::class);
}
return $this->documentTemplateService;
}
/**
* Retrieve InvestmentService instance.
*
* @return InvestmentService
*/
protected function getInvestmentService(): InvestmentService
{
if (null === $this->investmentService) {
$this->investmentService = resolve(InvestmentService::class);
}
return $this->investmentService;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment