508 lines
17 KiB
PHP
508 lines
17 KiB
PHP
<?php
|
|
|
|
namespace App\Models\Payment;
|
|
|
|
use App\Enums\GeneralConfigType;
|
|
use App\Enums\Order\PaymentModeEnum;
|
|
use App\Enums\Order\PaymentStatus;
|
|
use App\Enums\Order\TransactionStatus;
|
|
use App\Enums\Order\TransactionType;
|
|
use App\Enums\Order\ValidationStatus;
|
|
use App\Lib\Helpers\ToolboxHelper;
|
|
use App\Lib\Payments\PaymentAdapter;
|
|
use App\Models\Shop\Order;
|
|
use App\Models\Site\GeneralConfig;
|
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
|
use Illuminate\Database\Eloquent\Model;
|
|
use Illuminate\Support\Facades\Session;
|
|
use Illuminate\Support\Str;
|
|
use Kwaadpepper\Enum\Traits\CastsEnums;
|
|
use Mccarlosen\LaravelMpdf\LaravelMpdf;
|
|
use Money\Currency;
|
|
use Money\Money;
|
|
use Mpdf\Config\FontVariables;
|
|
|
|
/**
|
|
* Transaction
|
|
*
|
|
* @property-read string $reference The transaction unique reference string.
|
|
* @property-read \Money\Currency $currency The transaction currency.
|
|
* @property \Money\Money $amount The transaction amount.
|
|
* @property-read string $foreign_id The transaction id of the selected payment system.
|
|
* @property-read string $foreign_refund_id The refund transaction id of the selected
|
|
* payment system, (for webhook usage)
|
|
*
|
|
* @property \App\Enums\Order\PaymentModeEnum $payment_mode The Payment mode for this transaction.
|
|
* @property \App\Enums\Order\TransactionStatus $status The transaction status.
|
|
* @property \App\Enums\Order\TransactionType $type The transaction type (debit/credit) for customer.
|
|
*
|
|
* @package App\Models
|
|
*
|
|
*/
|
|
class Transaction extends Model
|
|
{
|
|
use CastsEnums;
|
|
use HasFactory;
|
|
|
|
/**
|
|
* The attributes that should be cast to Enums.
|
|
*
|
|
* @var array
|
|
*/
|
|
protected $enumCasts = [
|
|
'payment_mode' => PaymentModeEnum::class,
|
|
'status' => TransactionStatus::class,
|
|
'type' => TransactionType::class,
|
|
];
|
|
|
|
/**
|
|
* The attributes that are mass assignable.
|
|
*
|
|
* ! No attributes can be filled, TAKE THIS SERIOUSLY !
|
|
*
|
|
* @var array
|
|
*/
|
|
protected $fillable = [];
|
|
|
|
/**
|
|
* Perform any actions required after the model boots.
|
|
*
|
|
* @return void
|
|
*/
|
|
protected static function booted()
|
|
{
|
|
// * EVENT CLOSURES.
|
|
static::creating(function (self $model) {
|
|
$model->assertHasOrder();
|
|
// * Référence unique par jour.
|
|
$model->reference = ToolboxHelper::getUniqueReference(self::class);
|
|
$model->mutateOrderStatus();
|
|
});
|
|
static::updating(function (self $model) {
|
|
$model->assertHasOrder();
|
|
if (!$model->reference) {
|
|
throw new \RuntimeException('Missing reference');
|
|
}
|
|
$model->mutateOrderStatus();
|
|
});
|
|
static::deleting(function () {
|
|
throw new \RuntimeException('Removing a transaction is not allowed');
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Get the route key for the model.
|
|
*
|
|
* @return string
|
|
*/
|
|
public function getRouteKeyName()
|
|
{
|
|
return 'reference';
|
|
}
|
|
|
|
// * METHODS
|
|
|
|
/**
|
|
* Create a new instance of the given model.
|
|
*
|
|
* @param \App\Enums\Order\PaymentModeEnum $mode
|
|
* @param \App\Enums\Order\TransactionType $type
|
|
* @param integer $amount
|
|
* @param array $attributes
|
|
* @return static
|
|
* @throws \RuntimeException If amount is below 0.
|
|
*/
|
|
public static function getNewInstance(
|
|
PaymentModeEnum $mode,
|
|
TransactionType $type,
|
|
int $amount = 0,
|
|
array $attributes = []
|
|
): self {
|
|
// phpcs:enable
|
|
if ($amount < 0) {
|
|
throw new \RuntimeException('Amount cannot be negative');
|
|
}
|
|
|
|
$transaction = new self($attributes);
|
|
$transaction->amount = new Money($amount, app('phpMoneyCurrency'));
|
|
$transaction->status = TransactionStatus::pending();
|
|
$transaction->type = $type;
|
|
$transaction->payment_mode = $mode;
|
|
return $transaction;
|
|
}
|
|
|
|
/**
|
|
* Return payment adpater
|
|
*
|
|
* @return \App\Lib\Payments\PaymentAdapter
|
|
*/
|
|
public function getAdapter(): PaymentAdapter
|
|
{
|
|
// * Singleton adpater
|
|
if (!app($this->payment_mode->adapter)) {
|
|
\app()->singleton($this->payment_mode->adapter, function () {
|
|
return ($this->payment_mode->adapter)();
|
|
});
|
|
}
|
|
|
|
return app($this->payment_mode->adapter);
|
|
}
|
|
|
|
/**
|
|
* Get updated current session order
|
|
*
|
|
* @return null|\App\Models\Payment\Transaction
|
|
*/
|
|
public static function getCurrentTransaction(): ?self
|
|
{
|
|
/** @var \App\Models\Payment\Transaction|null $transaction */
|
|
$transaction = Session::get('currentTransaction');
|
|
return $transaction;
|
|
}
|
|
|
|
/**
|
|
* Fetch the associated order.
|
|
*
|
|
* @return \App\Models\Shop\Order
|
|
*/
|
|
protected function fetchOrder(): Order
|
|
{
|
|
return $this->order;
|
|
}
|
|
|
|
/**
|
|
* Assert is associated with an actual order
|
|
*
|
|
* @return void
|
|
* @throws \RuntimeException If no order is associated.
|
|
*/
|
|
public function assertHasOrder(): void
|
|
{
|
|
if (!$this->order()->exists()) {
|
|
throw new \RuntimeException("Transaction associated with an order, id '{$this->id}' that does not exists");
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Set related Order payment status using this transaction actual status.
|
|
*
|
|
* @return void
|
|
* @throws \RuntimeException If this transaction is not associated with and order.
|
|
*/
|
|
public function mutateOrderStatus(): void
|
|
{
|
|
// ! Dont handle this here.
|
|
if ($this->type->equals(TransactionType::credit())) {
|
|
return;
|
|
}
|
|
/** @var \App\Models\Shop\Order $relatedOrder */
|
|
if (!($relatedOrder = $this->order()->first())) {
|
|
throw new \RuntimeException("Any transaction has to be associated with an order, id '{$this->id}'");
|
|
}
|
|
// * Ignore pending here as default status
|
|
switch ($this->status) {
|
|
case TransactionStatus::validated():
|
|
$relatedOrder->payment_status = PaymentStatus::payed();
|
|
if (!$this->payment_mode->equals(PaymentModeEnum::inShop())) {
|
|
// * La commande est automatiquement validée lors d'un paiement en ligne.
|
|
$relatedOrder->validation_status = ValidationStatus::validated();
|
|
}
|
|
break;
|
|
case TransactionStatus::failed():
|
|
// * Just set failure on any failed transaction.
|
|
$relatedOrder->payment_status = PaymentStatus::failed();
|
|
break;
|
|
}
|
|
$relatedOrder->saveQuietly();
|
|
}
|
|
|
|
/**
|
|
* Get the foreign key name as formatted in DB
|
|
*
|
|
* @param \App\Enums\Order\PaymentModeEnum $payment_mode_enum
|
|
* @param string $foreign_id
|
|
* @return string
|
|
*/
|
|
public static function getPaymentForeignKeyName(PaymentModeEnum $payment_mode_enum, string $foreign_id): string
|
|
{
|
|
return "{$payment_mode_enum->value}_{$foreign_id}";
|
|
}
|
|
|
|
/**
|
|
* Set the payment foreign_id
|
|
*
|
|
* @param string $foreign_id The payment service unique id for this transaction.
|
|
* @return void
|
|
* @throws \RuntimeException The $foreign_id constrains are not matched.
|
|
*/
|
|
public function assignPaymentId(string $foreign_id): void
|
|
{
|
|
if (!\is_numeric($this->payment_mode->value)) {
|
|
throw new \RuntimeException('Assign foreign_id needs to have a payment mode');
|
|
}
|
|
$foreign_id = self::getPaymentForeignKeyName($this->payment_mode, $foreign_id);
|
|
if ($this->query()->where('foreign_id', $foreign_id)->where('id', '!=', $this->id)->exists()) {
|
|
throw new \RuntimeException(
|
|
"Assign foreign_id needs to have a unique value, '{$foreign_id}' already exists"
|
|
);
|
|
}
|
|
$this->attributes['foreign_id'] = $foreign_id;
|
|
}
|
|
|
|
/**
|
|
* Set the payment foreign_refund_id
|
|
*
|
|
* @param string $foreign_refund_id The payment service unique id for this transaction.
|
|
* @return void
|
|
* @throws \RuntimeException The $foreign_refund_id constrains are not matched.
|
|
*/
|
|
public function assignRefundId(string $foreign_refund_id): void
|
|
{
|
|
if (!\is_numeric($this->payment_mode->value)) {
|
|
throw new \RuntimeException('Assign foreign_refund_id needs to have a payment mode');
|
|
}
|
|
$foreign_refund_id = self::getPaymentForeignKeyName($this->payment_mode, $foreign_refund_id);
|
|
if ($this->query()->where('foreign_refund_id', $foreign_refund_id)->where('id', '!=', $this->id)->exists()) {
|
|
throw new \RuntimeException(
|
|
"Assign foreign_id needs to have a unique value, '{$foreign_refund_id}' already exists"
|
|
);
|
|
}
|
|
$this->attributes['foreign_refund_id'] = $foreign_refund_id;
|
|
}
|
|
|
|
/**
|
|
* Generate Invoice
|
|
*
|
|
* @param string $title
|
|
* @return \Mccarlosen\LaravelMpdf\LaravelMpdf
|
|
*/
|
|
public function generateInvoice(string $title): LaravelMpdf
|
|
{
|
|
/** @var \App\Models\GeneralConfig */
|
|
$config = GeneralConfig::getCached()->where('type', GeneralConfigType::global())->firstOrFail();
|
|
|
|
$additionalData = $config->data;
|
|
|
|
/** @var \App\Models\Shop\Order */
|
|
$order = $this->order;
|
|
|
|
/** @var \Mccarlosen\LaravelMpdf\LaravelMpdfWrapper */
|
|
$mpdf = app('laravel-mpdf');
|
|
|
|
$defaultFontConfig = (new FontVariables())->getDefaults();
|
|
$fontData = $defaultFontConfig['fontdata'];
|
|
|
|
$isCompleteRefunded = false;
|
|
$isRefunded = false;
|
|
if ($this->type->equals(TransactionType::credit())) {
|
|
$zeroMoney = new Money(0, app('phpMoneyCurrency'));
|
|
/** */
|
|
$isCompleteRefunded = (new Money($order->total_ttc, app('phpMoneyCurrency')))
|
|
->subtract($this->amount)->lessThanOrEqual($zeroMoney) or
|
|
\optional($order->transactions()->orderBy('created_at')->where('type', TransactionType::credit())
|
|
->latest()->pluck('id')->last())->id === $this->id;
|
|
$isRefunded = $isCompleteRefunded ?: $this->type->equals(
|
|
TransactionType::credit()
|
|
);
|
|
}
|
|
|
|
/**
|
|
* * Because of Stripe reusing transactions, therefore use prefix.
|
|
*/
|
|
$transactionReferenceSuffix = $order->payment_status->equals(PaymentStatus::refunded()) ? '_1' : '';
|
|
|
|
// * Transaction is Completely Refund.
|
|
$invoiceBasketItems = $isCompleteRefunded ? \collect($order->basket) : null;
|
|
// * Transaction is Refund Partial.
|
|
$invoiceBasketItems = $invoiceBasketItems ?? ($isRefunded ? $order->getPartialRefundedBasket() : null);
|
|
// * Transaction is Completed.
|
|
$invoiceBasketItems = $invoiceBasketItems ?? \collect($order->basket);
|
|
|
|
/**
|
|
* 7.5pt 10px
|
|
* 9pt 12px
|
|
* 10.5pt 14px
|
|
* 12pt 16px
|
|
* 13.5pt 18px
|
|
*/
|
|
/** @var \Mccarlosen\LaravelMpdf\LaravelMpdf */
|
|
$pdf = $mpdf->loadView(
|
|
'back::documents.order-invoice',
|
|
$this->toArray(),
|
|
[
|
|
'title' => $title,
|
|
'billing' => !empty($order->billing->firstname) ? $order->billing : $order->shipping,
|
|
'shipping' => $order->shipping,
|
|
'logo' => \public_path(resize($additionalData->logo->value ?? '', 'BoutiqueLogo')),
|
|
'introLines' => \collect($additionalData)->map(function (object $input, string $inputName) {
|
|
return Str::substr($inputName, 0, -1) === 'invoice_line_' ? $input->value : null;
|
|
})->filter()->all(),
|
|
'orderReference' => $order->reference,
|
|
'transactionReference' => $this->reference . $transactionReferenceSuffix,
|
|
'date' => $this->updated_at,
|
|
'transactionType' => $this->type,
|
|
'basket' => $invoiceBasketItems,
|
|
'totalTtc' => $invoiceBasketItems->sum('subtotal'),
|
|
'totalTaxes' => $invoiceBasketItems->sum('tax')
|
|
],
|
|
[
|
|
'mode' => 'utf-8',
|
|
'format' => 'A4',
|
|
// * 10.5pt = 14px
|
|
'default_font_size' => 10.5,
|
|
'default_font' => 'roboto',
|
|
'margin_left' => 5,
|
|
'margin_right' => 5,
|
|
'margin_top' => 5,
|
|
'margin_bottom' => 5,
|
|
'margin_header' => 0,
|
|
'margin_footer' => 5,
|
|
'orientation' => 'P',
|
|
'title' => $title,
|
|
'author' => \config('app.name'),
|
|
'watermark' => '',
|
|
'show_watermark' => false,
|
|
'watermark_font' => 'sans-serif',
|
|
'display_mode' => 'fullpage',
|
|
'watermark_text_alpha' => 0.1,
|
|
'custom_font_dir' => '',
|
|
'custom_font_data' => [],
|
|
'auto_language_detection' => false,
|
|
'temp_dir' => rtrim(sys_get_temp_dir(), DIRECTORY_SEPARATOR),
|
|
'pdfa' => false,
|
|
'pdfaauto' => false,
|
|
'fontdata' => $fontData + [
|
|
'roboto' => [
|
|
'R' => \resource_path('assets/fonts/bo/roboto/RobotoMono-Regular.ttf'),
|
|
'B' => \resource_path('assets/fonts/bo/roboto/RobotoMono-Bold.ttf'),
|
|
'I' => \resource_path('assets/fonts/bo/roboto/RobotoMono-Italic.ttf'),
|
|
'BI' => \resource_path('assets/fonts/bo/roboto/RobotoMono-BoldItalic.ttf')
|
|
]
|
|
]
|
|
]
|
|
);
|
|
return $pdf;
|
|
}
|
|
|
|
// * GETTER
|
|
|
|
/**
|
|
* Get the transation amount
|
|
*
|
|
* @return \Money\Money
|
|
*/
|
|
public function getAmountAttribute(): Money
|
|
{
|
|
return new Money($this->attributes['amount'], $this->currency);
|
|
}
|
|
|
|
/**
|
|
* Get the transation currency
|
|
*
|
|
* @return \Money\Currency
|
|
*/
|
|
public function getCurrencyAttribute(): Currency
|
|
{
|
|
return new Currency(Str::upper($this->attributes['currency']));
|
|
}
|
|
|
|
/**
|
|
* Get the foreign_id
|
|
*
|
|
* @return string|null
|
|
* @throws \RuntimeException If the prefix does not match the payment mode.
|
|
*/
|
|
public function getForeignIdAttribute(): ?string
|
|
{
|
|
if (
|
|
!empty($this->attributes['foreign_id']) and
|
|
\strpos($this->attributes['foreign_id'] ?? '', "{$this->payment_mode->value}_") !== 0
|
|
) {
|
|
throw new \RuntimeException(
|
|
"The payment mode {$this->payment_mode->label} does not match the foreign_id prefix"
|
|
);
|
|
}
|
|
return Str::replace("{$this->payment_mode->value}_", '', $this->attributes['foreign_id'] ?? '');
|
|
}
|
|
|
|
/**
|
|
* Get the foreign_refund_id
|
|
*
|
|
* @return string|null
|
|
* @throws \RuntimeException If the prefix does not match the payment mode.
|
|
*/
|
|
public function getForeignRefundIdAttribute(): ?string
|
|
{
|
|
if (empty($this->attributes['foreign_refund_id'])) {
|
|
return null;
|
|
}
|
|
if (
|
|
!empty($this->attributes['foreign_refund_id']) and
|
|
\strpos($this->attributes['foreign_refund_id'] ?? '', "{$this->payment_mode->value}_") !== 0
|
|
) {
|
|
throw new \RuntimeException(
|
|
"The payment mode {$this->payment_mode->label} does not match the foreign_refund_id prefix"
|
|
);
|
|
}
|
|
return Str::replace("{$this->payment_mode->value}_", '', $this->attributes['foreign_refund_id']);
|
|
}
|
|
|
|
// * SETTER
|
|
|
|
/**
|
|
* Set the transation amount
|
|
*
|
|
* @param \Money\Money $value
|
|
* @return void
|
|
*/
|
|
public function setAmountAttribute(Money $value): void
|
|
{
|
|
$this->attributes['amount'] = \intval($value->getAmount());
|
|
$this->attributes['currency'] = \strval($value->getCurrency());
|
|
}
|
|
|
|
/**
|
|
* Set the transation currency
|
|
*
|
|
* @return void
|
|
* @throws \Error In any case, this is not allowed.
|
|
*/
|
|
public function setCurrencyAttribute(): void
|
|
{
|
|
throw new \Error('Settings manually currency is not allowed');
|
|
}
|
|
|
|
/**
|
|
* Set the transation foreign_id
|
|
*
|
|
* @return void
|
|
* @throws \Error In any case, this is not allowed.
|
|
*/
|
|
public function setForeignIdAttribute(): void
|
|
{
|
|
throw new \Error('Settings manually foreign_id is not allowed');
|
|
}
|
|
|
|
/**
|
|
* Set the transation foreign_refund_id
|
|
*
|
|
* @return void
|
|
* @throws \Error In any case, this is not allowed.
|
|
*/
|
|
public function setForeignRefundIdAttribute(): void
|
|
{
|
|
throw new \Error('Settings manually foreign_refund_id is not allowed');
|
|
}
|
|
|
|
// * RELATIONSHIPS
|
|
|
|
/**
|
|
* BelongsTo order relation
|
|
*
|
|
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
|
|
*/
|
|
public function order()
|
|
{
|
|
return $this->belongsTo(Order::class);
|
|
}
|
|
}
|