refactor: replace custom CRM with Monica fork
Some checks failed
Build & Push Monica Image to Gitea Registry / build-and-push (push) Failing after 9s

This commit is contained in:
Kroonk
2026-05-22 16:19:55 +02:00
parent 38cc4c09ca
commit a213a1dba0
2215 changed files with 242567 additions and 6015 deletions

View File

@@ -0,0 +1,73 @@
<?php
namespace App\Traits;
use Illuminate\Support\Arr;
use App\Helpers\MoneyHelper;
use App\Models\Settings\Currency;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
trait AmountFormatter
{
/**
* Get the currency record associated with the debt.
*
* @return BelongsTo
*/
public function currency()
{
return $this->belongsTo(Currency::class);
}
/**
* Set exchange value.
*
* @return void
*/
public function setAmountAttribute($value)
{
$this->attributes['amount'] = MoneyHelper::parseInput($value, $this->currency);
}
/**
* Get exchange value.
*
* @return string|null
*/
public function getAmountAttribute(): ?string
{
if (! ($amount = Arr::get($this->attributes, 'amount', null))) {
return null;
}
return MoneyHelper::exchangeValue($amount, $this->currency);
}
/**
* Get value of amount (without currency).
*
* @return string
*/
public function getValueAttribute(): string
{
if (! ($amount = Arr::get($this->attributes, 'amount', null))) {
return '';
}
return MoneyHelper::getValue($amount, $this->currency);
}
/**
* Get display value: amount with currency.
*
* @return string
*/
public function getDisplayValueAttribute(): string
{
if (! ($amount = Arr::get($this->attributes, 'amount', null))) {
return '';
}
return MoneyHelper::format($amount, $this->currency);
}
}

17
app/Traits/DAVFormat.php Normal file
View File

@@ -0,0 +1,17 @@
<?php
namespace App\Traits;
trait DAVFormat
{
/**
* Formats and returns a string for DAV Card/Cal.
*
* @param null|string $value
* @return null|string
*/
private function formatValue(?string $value): ?string
{
return ! empty($value) ? str_replace('\;', ';', trim($value)) : null;
}
}

27
app/Traits/HasUuid.php Normal file
View File

@@ -0,0 +1,27 @@
<?php
namespace App\Traits;
use Illuminate\Support\Str;
trait HasUuid
{
/**
* Get model's Uuid.
*
* @return string
*/
public function getUuidAttribute(): string
{
if (! isset($this->attributes['uuid']) || empty($this->attributes['uuid']) || $this->attributes['uuid'] == null) {
return (string) tap(Str::uuid()->toString(), function ($uuid) {
$this->forceFill([
'uuid' => $uuid,
]);
$this->save(['timestamps' => false]);
});
}
return (string) $this->attributes['uuid'];
}
}

37
app/Traits/Hasher.php Normal file
View File

@@ -0,0 +1,37 @@
<?php
namespace App\Traits;
use App\Services\Instance\IdHasher;
use Illuminate\Database\Eloquent\Model;
/** @psalm-implements \App\Interfaces\Hashing */
trait Hasher
{
/**
* @psalm-suppress MethodSignatureMustProvideReturnType
*
* @return string
*/
public function getRouteKey()
{
return app(IdHasher::class)->encodeId(parent::getRouteKey());
}
public function resolveRouteBinding($value, $field = null): ?Model
{
$id = $this->decodeId($value);
return parent::resolveRouteBinding($id, $field);
}
protected function decodeId($value)
{
return app(IdHasher::class)->decodeId($value);
}
public function hashID()
{
return $this->getRouteKey();
}
}

View File

@@ -0,0 +1,46 @@
<?php
namespace App\Traits;
use App\Models\Journal\JournalEntry;
use Illuminate\Database\Eloquent\Relations\MorphOne;
use Illuminate\Database\Eloquent\Relations\MorphMany;
trait Journalable
{
/**
* Get all journal entries.
*
* @return MorphMany
*/
public function journalEntries()
{
return $this->morphMany(JournalEntry::class, 'journalable');
}
/**
* Get the journal record associated.
*
* @return MorphOne
*/
public function journalEntry()
{
return $this->morphOne(JournalEntry::class, 'journalable');
}
/**
* Delete the Journal Entry associated with the given object.
*
* @return bool
*/
public function deleteJournalEntry()
{
if ($this->journalEntry) {
$this->journalEntry->delete();
return true;
}
return false;
}
}

View File

@@ -0,0 +1,193 @@
<?php
namespace App\Traits;
use Illuminate\Http\Request;
use Illuminate\Http\JsonResponse;
use Illuminate\Contracts\Validation\Validator;
trait JsonRespondController
{
/**
* @var int
*/
protected $httpStatusCode = 200;
/**
* @var int
*/
protected $errorCode;
/**
* Get HTTP status code of the response.
*
* @return int
*/
public function getHTTPStatusCode()
{
return $this->httpStatusCode;
}
/**
* Set HTTP status code of the response.
*
* @param int $statusCode
* @return self
*/
public function setHTTPStatusCode($statusCode)
{
$this->httpStatusCode = $statusCode;
return $this;
}
/**
* Get error code of the response.
*
* @return int
*/
public function getErrorCode()
{
return $this->errorCode;
}
/**
* Set error code of the response.
*
* @param int $errorCode
* @return self
*/
public function setErrorCode($errorCode)
{
$this->errorCode = $errorCode;
return $this;
}
/**
* Sends a JSON to the consumer.
*
* @param array $data
* @param array $headers [description]
* @return JsonResponse
*/
public function respond($data, $headers = [])
{
return response()->json($data, $this->getHTTPStatusCode(), $headers);
}
/**
* Sends a response not found (404) to the request.
* Error Code = 31.
*
* @return JsonResponse
*/
public function respondNotFound()
{
return $this->setHTTPStatusCode(404)
->setErrorCode(31)
->respondWithError();
}
/**
* Sends an error when the validator failed.
* Error Code = 32.
*
* @param Validator $validator
* @return JsonResponse
*/
public function respondValidatorFailed(Validator $validator)
{
return $this->setHTTPStatusCode(422)
->setErrorCode(32)
->respondWithError($validator->errors()->all());
}
/**
* Sends an error when the query didn't have the right parameters for
* creating an object.
* Error Code = 33.
*
* @param string $message
* @return JsonResponse
*/
public function respondNotTheRightParameters($message = null)
{
return $this->setHTTPStatusCode(500)
->setErrorCode(33)
->respondWithError($message);
}
/**
* Sends a response invalid query (http 500) to the request.
* Error Code = 40.
*
* @param string $message
* @return JsonResponse
*/
public function respondInvalidQuery($message = null)
{
return $this->setHTTPStatusCode(500)
->setErrorCode(40)
->respondWithError($message);
}
/**
* Sends an error when the query contains invalid parameters.
* Error Code = 41.
*
* @param string $message
* @return JsonResponse
*/
public function respondInvalidParameters($message = null)
{
return $this->setHTTPStatusCode(422)
->setErrorCode(41)
->respondWithError($message);
}
/**
* Sends a response unauthorized (401) to the request.
* Error Code = 42.
*
* @param string $message
* @return JsonResponse
*/
public function respondUnauthorized($message = null)
{
return $this->setHTTPStatusCode(401)
->setErrorCode(42)
->respondWithError($message);
}
/**
* Sends a response with error.
*
* @param string|array $message
* @return JsonResponse
*/
public function respondWithError($message = null)
{
return $this->respond([
'error' => [
'message' => $message ?? config('api.error_codes.'.$this->getErrorCode()),
'error_code' => $this->getErrorCode(),
],
]);
}
/**
* Sends a response that the object has been deleted, and also indicates
* the id of the object that has been deleted.
*
* @param int $id
* @return JsonResponse
*/
public function respondObjectDeleted($id)
{
return $this->respond([
'deleted' => true,
'id' => $id,
]);
}
}

76
app/Traits/Searchable.php Normal file
View File

@@ -0,0 +1,76 @@
<?php
namespace App\Traits;
use App\Helpers\DBHelper;
use Illuminate\Database\Eloquent\Builder;
trait Searchable
{
/**
* Search for needle in the columns defined by $searchable_columns.
*
* @param Builder $builder query builder
* @param string $needle
* @param int $accountId
* @param string $orderByColumn
* @param string $orderByDirection
* @param string $sortOrder
* @return Builder|null
*/
public function scopeSearch(Builder $builder, string $needle, int $accountId, string $orderByColumn, string $orderByDirection = 'asc', string $sortOrder = null): ?Builder
{
if ($this->searchable_columns == null) {
return null;
}
$searchableColumns = array_map(function ($column) {
return DBHelper::getTable($this->getTable()).".`$column`";
}, $this->searchable_columns);
$queryString = $this->buildQuery($searchableColumns, $needle);
$builder->whereRaw(DBHelper::getTable($this->getTable()).".`account_id` = $accountId")
->whereRaw("( $queryString )")
->orderBy($orderByColumn, $orderByDirection);
if ($sortOrder) {
$builder->sortedBy($sortOrder);
}
$builder->select(array_map(function ($column) {
return "{$this->getTable()}.$column";
}, $this->return_from_search));
return $builder;
}
/**
* Build a query based on the array that contains column names.
*
* @param array $array
* @param string $searchTerm
* @return string
*/
private function buildQuery(array $array, string $searchTerm): string
{
$first = true;
$queryString = '';
$searchTerms = explode(' ', $searchTerm);
foreach ($searchTerms as $searchTerm) {
$searchTerm = DBHelper::connection()->getPdo()->quote('%'.$searchTerm.'%');
foreach ($array as $column) {
if ($first) {
$first = false;
} else {
$queryString .= ' OR ';
}
$queryString .= $column.' LIKE '.$searchTerm;
}
}
return $queryString;
}
}

57
app/Traits/StripeCall.php Normal file
View File

@@ -0,0 +1,57 @@
<?php
namespace App\Traits;
use App\Exceptions\StripeException;
use Illuminate\Support\Facades\Log;
trait StripeCall
{
/**
* Call stripe.
*
* @template TValue
*
* @param (callable(): TValue) $callback
* @return TValue
*/
private function stripeCall($callback)
{
try {
return $callback();
} catch (\Stripe\Exception\CardException $e) {
// Since it's a decline, \Stripe\Error\Card will be caught
$body = $e->getJsonBody();
$err = $body['error'];
$errorMessage = trans('settings.stripe_error_card', ['message' => $err['message']]);
Log::error(__CLASS__.' '.__FUNCTION__.': Stripe card decline error: '.$e->getMessage(), ['body' => $e->getJsonBody(), $e]);
} catch (\Stripe\Exception\RateLimitException $e) {
// Too many requests made to the API too quickly
$errorMessage = trans('settings.stripe_error_rate_limit');
Log::error(__CLASS__.' '.__FUNCTION__.': Stripe RateLimit error: '.$e->getMessage(), ['body' => $e->getJsonBody(), $e]);
} catch (\Stripe\Exception\InvalidRequestException $e) {
// Invalid parameters were supplied to Stripe's API
$errorMessage = trans('settings.stripe_error_invalid_request');
Log::error(__CLASS__.' '.__FUNCTION__.': Stripe InvalidRequest error: '.$e->getMessage(), ['body' => $e->getJsonBody(), $e]);
} catch (\Stripe\Exception\AuthenticationException $e) {
// Authentication with Stripe's API failed
// (maybe you changed API keys recently)
$errorMessage = trans('settings.stripe_error_authentication');
Log::error(__CLASS__.' '.__FUNCTION__.': Stripe Authentication error: '.$e->getMessage(), ['body' => $e->getJsonBody(), $e]);
} catch (\Stripe\Exception\ApiConnectionException $e) {
// Network communication with Stripe failed
$errorMessage = trans('settings.stripe_error_api_connection_error');
Log::error(__CLASS__.' '.__FUNCTION__.': Stripe ApiConnection error: '.$e->getMessage(), ['body' => $e->getJsonBody(), $e]);
} catch (\Stripe\Exception\ApiErrorException $e) {
$errorMessage = $e->getMessage();
Log::error(__CLASS__.' '.__FUNCTION__.': Stripe error: '.$e->getMessage(), ['body' => $e->getJsonBody(), $e]);
} catch (\Laravel\Cashier\Exceptions\IncompletePayment $e) {
throw $e;
} catch (\Exception $e) {
$errorMessage = $e->getMessage();
Log::error(__CLASS__.' '.__FUNCTION__.': Stripe error: '.$e->getMessage(), [$e]);
}
throw new StripeException($errorMessage);
}
}

146
app/Traits/Subscription.php Normal file
View File

@@ -0,0 +1,146 @@
<?php
namespace App\Traits;
use Laravel\Cashier\Billable;
use App\Helpers\InstanceHelper;
trait Subscription
{
use Billable, StripeCall;
/**
* Process the upgrade payment.
*
* @param string $payment_method
* @param string $planName
* @return bool|string
*/
public function subscribe(string $payment_method, string $planName)
{
$plan = InstanceHelper::getPlanInformationFromConfig($planName);
if ($plan === null) {
abort(404);
}
return $this->stripeCall(function () use ($payment_method, $plan) {
$this->newSubscription($plan['name'], $plan['id'])
->create($payment_method, [
'email' => auth()->user()->email,
]);
return true;
});
}
/**
* Update an existing subscription.
*
* @param string $planName
* @param \Laravel\Cashier\Subscription $subscription
* @return \Laravel\Cashier\Subscription
*/
public function updateSubscription(string $planName, \Laravel\Cashier\Subscription $subscription)
{
$oldPlan = $subscription->stripe_price;
$plan = InstanceHelper::getPlanInformationFromConfig($planName);
if ($plan === null) {
abort(404);
}
if ($oldPlan === $planName) {
// No change
return $subscription;
}
$subscription = $this->stripeCall(function () use ($subscription, $plan) {
return $subscription->swap($plan['id']);
});
if ($subscription->stripe_price !== $oldPlan && $subscription->stripe_price === $plan['id']) {
$subscription->forceFill([
'name' => $plan['name'],
])->save();
}
return $subscription;
}
/**
* Check if the account is currently subscribed to a plan.
*
* @return bool
*/
public function isSubscribed()
{
if ($this->has_access_to_paid_version_for_free) {
return true;
}
return $this->getSubscribedPlan() !== null;
}
/**
* Get the subscription the account is subscribed to.
*
* @return \Laravel\Cashier\Subscription|null
*/
public function getSubscribedPlan()
{
return $this->subscriptions()->recurring()->first();
}
/**
* Get the id of the plan the account is subscribed to.
*
* @return string
*/
public function getSubscribedPlanId()
{
$plan = $this->getSubscribedPlan();
return is_null($plan) ? '' : $plan->stripe_price;
}
/**
* Get the friendly name of the plan the account is subscribed to.
*
* @return string|null
*/
public function getSubscribedPlanName(): ?string
{
$plan = $this->getSubscribedPlan();
return is_null($plan) ? null : $plan->name;
}
/**
* Cancel the plan the account is subscribed to.
*
* @return bool|string
*/
public function subscriptionCancel()
{
$plan = $this->getSubscribedPlan();
if (! is_null($plan)) {
return $this->stripeCall(function () use ($plan) {
$plan->cancelNow();
return true;
});
}
return false;
}
/**
* Check if the account has invoices linked to this account.
*
* @return bool
*/
public function hasInvoices()
{
return $this->subscriptions()->count() > 0;
}
}

26
app/Traits/WithUser.php Normal file
View File

@@ -0,0 +1,26 @@
<?php
namespace App\Traits;
use App\Models\User\User;
trait WithUser
{
/**
* @var \App\Models\User\User
*/
protected $user;
/**
* Initialize.
*
* @param User $user
* @return self
*/
public function init(User $user): self
{
$this->user = $user;
return $this;
}
}