php-commerce/Models/CreditNote.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

256 lines
6.3 KiB
PHP

<?php
namespace Core\Mod\Commerce\Models;
use Core\Mod\Tenant\Models\User;
use Core\Mod\Tenant\Models\Workspace;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Spatie\Activitylog\LogOptions;
use Spatie\Activitylog\Traits\LogsActivity;
/**
* Credit Note model for tracking credits issued to users.
*
* Credit notes can be issued as:
* - General credits (goodwill, promotional)
* - Partial refunds as store credit
* - Applied to orders to reduce payment amount
*
* @property int $id
* @property int $workspace_id
* @property int $user_id
* @property int|null $order_id
* @property int|null $refund_id
* @property string $reference_number
* @property float $amount
* @property string $currency
* @property string $reason
* @property string|null $description
* @property string $status
* @property float $amount_used
* @property int|null $applied_to_order_id
* @property \Carbon\Carbon|null $issued_at
* @property \Carbon\Carbon|null $applied_at
* @property \Carbon\Carbon|null $voided_at
* @property int|null $issued_by
* @property int|null $voided_by
* @property array|null $metadata
*/
class CreditNote extends Model
{
use HasFactory;
use LogsActivity;
protected $fillable = [
'workspace_id',
'user_id',
'order_id',
'refund_id',
'reference_number',
'amount',
'currency',
'reason',
'description',
'status',
'amount_used',
'applied_to_order_id',
'issued_at',
'applied_at',
'voided_at',
'issued_by',
'voided_by',
'metadata',
];
protected $casts = [
'amount' => 'decimal:2',
'amount_used' => 'decimal:2',
'metadata' => 'array',
'issued_at' => 'datetime',
'applied_at' => 'datetime',
'voided_at' => 'datetime',
];
// Relationships
public function workspace(): BelongsTo
{
return $this->belongsTo(Workspace::class);
}
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
public function order(): BelongsTo
{
return $this->belongsTo(Order::class);
}
public function refund(): BelongsTo
{
return $this->belongsTo(Refund::class);
}
public function appliedToOrder(): BelongsTo
{
return $this->belongsTo(Order::class, 'applied_to_order_id');
}
public function issuedByUser(): BelongsTo
{
return $this->belongsTo(User::class, 'issued_by');
}
public function voidedByUser(): BelongsTo
{
return $this->belongsTo(User::class, 'voided_by');
}
// Status helpers
public function isDraft(): bool
{
return $this->status === 'draft';
}
public function isIssued(): bool
{
return $this->status === 'issued';
}
public function isApplied(): bool
{
return $this->status === 'applied';
}
public function isPartiallyApplied(): bool
{
return $this->status === 'partially_applied';
}
public function isVoid(): bool
{
return $this->status === 'void';
}
public function isUsable(): bool
{
return in_array($this->status, ['issued', 'partially_applied']);
}
// Amount helpers
public function getRemainingAmount(): float
{
return max(0, $this->amount - $this->amount_used);
}
public function isFullyUsed(): bool
{
return $this->amount_used >= $this->amount;
}
// Actions
public function issue(?User $issuedBy = null): void
{
$this->update([
'status' => 'issued',
'issued_at' => now(),
'issued_by' => $issuedBy?->id,
]);
}
public function recordUsage(float $amount, ?Order $order = null): void
{
$newUsed = $this->amount_used + $amount;
$status = $newUsed >= $this->amount ? 'applied' : 'partially_applied';
$this->update([
'amount_used' => $newUsed,
'status' => $status,
'applied_to_order_id' => $order?->id ?? $this->applied_to_order_id,
'applied_at' => $status === 'applied' ? now() : $this->applied_at,
]);
}
public function void(?User $voidedBy = null): void
{
$this->update([
'status' => 'void',
'voided_at' => now(),
'voided_by' => $voidedBy?->id,
]);
}
// Scopes
public function scopeDraft($query)
{
return $query->where('status', 'draft');
}
public function scopeIssued($query)
{
return $query->where('status', 'issued');
}
public function scopeUsable($query)
{
return $query->whereIn('status', ['issued', 'partially_applied']);
}
public function scopeForWorkspace($query, int $workspaceId)
{
return $query->where('workspace_id', $workspaceId);
}
public function scopeForUser($query, int $userId)
{
return $query->where('user_id', $userId);
}
// Reference number generation
public static function generateReferenceNumber(): string
{
$prefix = 'CN';
$date = now()->format('Ymd');
$random = strtoupper(substr(md5(uniqid()), 0, 4));
return "{$prefix}-{$date}-{$random}";
}
// Reason helpers
public static function reasons(): array
{
return [
'partial_refund' => 'Partial refund as store credit',
'goodwill' => 'Goodwill gesture',
'service_issue' => 'Service issue compensation',
'promotional' => 'Promotional credit',
'billing_adjustment' => 'Billing adjustment',
'cancellation' => 'Subscription cancellation credit',
'other' => 'Other',
];
}
public function getReasonLabel(): string
{
return self::reasons()[$this->reason] ?? ucfirst(str_replace('_', ' ', $this->reason));
}
public function getActivitylogOptions(): LogOptions
{
return LogOptions::defaults()
->logOnly(['status', 'amount', 'amount_used', 'reason'])
->logOnlyDirty()
->dontSubmitEmptyLogs()
->setDescriptionForEvent(fn (string $eventName) => "Credit note {$eventName}");
}
}