php-commerce/Models/ReferralCommission.php
Snider a774f4e285 refactor: migrate namespace from Core\Commerce to Core\Mod\Commerce
Align commerce module with the monorepo module structure by updating
all namespaces to use the Core\Mod\Commerce convention. This change
supports the recent monorepo separation and ensures consistency with
other modules.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-27 16:23:12 +00:00

255 lines
6.5 KiB
PHP

<?php
declare(strict_types=1);
namespace Core\Mod\Commerce\Models;
use Core\Mod\Tenant\Models\User;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Spatie\Activitylog\LogOptions;
use Spatie\Activitylog\Traits\LogsActivity;
/**
* ReferralCommission model for tracking commission earnings.
*
* Each commission is linked to an order and tracks its maturation
* and payout status.
*
* @property int $id
* @property int $referral_id
* @property int $referrer_id
* @property int|null $order_id
* @property int|null $invoice_id
* @property float $order_amount
* @property float $commission_rate
* @property float $commission_amount
* @property string $currency
* @property string $status
* @property \Carbon\Carbon|null $matures_at
* @property \Carbon\Carbon|null $matured_at
* @property int|null $payout_id
* @property \Carbon\Carbon|null $paid_at
* @property string|null $notes
*/
class ReferralCommission extends Model
{
use LogsActivity;
protected $table = 'commerce_referral_commissions';
// Status constants
public const STATUS_PENDING = 'pending'; // Waiting to mature
public const STATUS_MATURED = 'matured'; // Can be withdrawn
public const STATUS_PAID = 'paid'; // Included in a payout
public const STATUS_CANCELLED = 'cancelled'; // Refunded/chargedback
// Default commission rate (percentage)
public const DEFAULT_COMMISSION_RATE = 10.00;
// Maturation periods (days after order)
public const MATURATION_CRYPTO = 14; // Crypto: 14 days (refund period)
public const MATURATION_CARD = 90; // Card: 90 days (chargeback period)
protected $fillable = [
'referral_id',
'referrer_id',
'order_id',
'invoice_id',
'order_amount',
'commission_rate',
'commission_amount',
'currency',
'status',
'matures_at',
'matured_at',
'payout_id',
'paid_at',
'notes',
];
protected $casts = [
'order_amount' => 'decimal:2',
'commission_rate' => 'decimal:2',
'commission_amount' => 'decimal:2',
'matures_at' => 'datetime',
'matured_at' => 'datetime',
'paid_at' => 'datetime',
];
// Relationships
public function referral(): BelongsTo
{
return $this->belongsTo(Referral::class);
}
public function referrer(): BelongsTo
{
return $this->belongsTo(User::class, 'referrer_id');
}
public function order(): BelongsTo
{
return $this->belongsTo(Order::class);
}
public function invoice(): BelongsTo
{
return $this->belongsTo(Invoice::class);
}
public function payout(): BelongsTo
{
return $this->belongsTo(ReferralPayout::class);
}
// Status helpers
public function isPending(): bool
{
return $this->status === self::STATUS_PENDING;
}
public function isMatured(): bool
{
return $this->status === self::STATUS_MATURED;
}
public function isPaid(): bool
{
return $this->status === self::STATUS_PAID;
}
public function isCancelled(): bool
{
return $this->status === self::STATUS_CANCELLED;
}
public function canMature(): bool
{
return $this->isPending() && $this->matures_at && $this->matures_at->isPast();
}
// Actions
/**
* Mark commission as matured.
*/
public function markMatured(): void
{
$this->update([
'status' => self::STATUS_MATURED,
'matured_at' => now(),
]);
}
/**
* Mark commission as paid (included in payout).
*/
public function markPaid(ReferralPayout $payout): void
{
$this->update([
'status' => self::STATUS_PAID,
'payout_id' => $payout->id,
'paid_at' => now(),
]);
}
/**
* Cancel commission (refund/chargeback).
*/
public function cancel(?string $reason = null): void
{
$this->update([
'status' => self::STATUS_CANCELLED,
'notes' => $reason,
]);
}
// Static factory
/**
* Calculate commission for an order.
*/
public static function calculateForOrder(
Referral $referral,
Order $order,
?float $commissionRate = null
): array {
$commissionRate = $commissionRate ?? self::DEFAULT_COMMISSION_RATE;
// Calculate commission on net order amount (after discount, before tax)
$netAmount = $order->subtotal - $order->discount_amount;
$commissionAmount = round($netAmount * ($commissionRate / 100), 2);
// Determine maturation date based on payment method
$gateway = $order->gateway ?? 'stripe';
$maturationDays = in_array($gateway, ['btcpay', 'bitcoin', 'crypto'])
? self::MATURATION_CRYPTO
: self::MATURATION_CARD;
return [
'referral_id' => $referral->id,
'referrer_id' => $referral->referrer_id,
'order_id' => $order->id,
'invoice_id' => $order->invoice?->id,
'order_amount' => $netAmount,
'commission_rate' => $commissionRate,
'commission_amount' => $commissionAmount,
'currency' => $order->currency,
'status' => self::STATUS_PENDING,
'matures_at' => now()->addDays($maturationDays),
];
}
// Scopes
public function scopePending($query)
{
return $query->where('status', self::STATUS_PENDING);
}
public function scopeMatured($query)
{
return $query->where('status', self::STATUS_MATURED);
}
public function scopePaid($query)
{
return $query->where('status', self::STATUS_PAID);
}
public function scopeWithdrawable($query)
{
return $query->where('status', self::STATUS_MATURED);
}
public function scopeReadyToMature($query)
{
return $query->pending()->where('matures_at', '<=', now());
}
public function scopeForReferrer($query, int $userId)
{
return $query->where('referrer_id', $userId);
}
public function scopeUnpaid($query)
{
return $query->whereNull('payout_id');
}
public function getActivitylogOptions(): LogOptions
{
return LogOptions::defaults()
->logOnly(['status', 'matured_at', 'payout_id', 'paid_at'])
->logOnlyDirty()
->dontSubmitEmptyLogs()
->setDescriptionForEvent(fn (string $eventName) => "Commission {$eventName}");
}
}