PaymentModeEnum::class, 'status' => TransactionStatus::class, 'type' => TransactionType::class, ]; /** * The attributes that should be cast. * * @var string[] */ protected $casts = [ 'needs_validation' => 'bool', 'items' => 'object' ]; /** * The model's attributes. * * @var array */ protected $attributes = [ 'items' => '{}', ]; /** * 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 The payment mode. * @param \App\Enums\Order\TransactionType $type The transaction type (credit or debit). * @param object $items The basket items concerned by the transaction. * @param integer $amount The amount of the transaction. * @param array $attributes Initial attributes to pass on to the model. * @return static * @throws \RuntimeException If amount is below 0. */ public static function getNewInstance( PaymentModeEnum $mode, TransactionType $type, object $items, 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; $transaction->needs_validation = false; $transaction->items = $items; 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']; /** * * Because of Stripe reusing transactions, therefore use prefix. */ $transactionReferenceSuffix = $order->payment_status->equals(PaymentStatus::refunded()) ? '_1' : ''; $invoiceBasketItems = \collect($this->items); /** * 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); } }