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

144 lines
3.4 KiB
PHP

<?php
namespace Core\Mod\Commerce\Models;
use Core\Mod\Tenant\Models\User;
use Core\Mod\Tenant\Models\Workspace;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Support\Str;
/**
* UsageEvent model - individual usage event before aggregation.
*
* @property int $id
* @property int $subscription_id
* @property int $meter_id
* @property int $workspace_id
* @property int $quantity
* @property \Carbon\Carbon $event_at
* @property string|null $idempotency_key
* @property int|null $user_id
* @property string|null $action
* @property array|null $metadata
*/
class UsageEvent extends Model
{
protected $table = 'commerce_usage_events';
protected $fillable = [
'subscription_id',
'meter_id',
'workspace_id',
'quantity',
'event_at',
'idempotency_key',
'user_id',
'action',
'metadata',
];
protected $casts = [
'quantity' => 'integer',
'event_at' => 'datetime',
'metadata' => 'array',
];
// Relationships
public function subscription(): BelongsTo
{
return $this->belongsTo(Subscription::class);
}
public function meter(): BelongsTo
{
return $this->belongsTo(UsageMeter::class, 'meter_id');
}
public function workspace(): BelongsTo
{
return $this->belongsTo(Workspace::class);
}
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
// Scopes
public function scopeForSubscription($query, int $subscriptionId)
{
return $query->where('subscription_id', $subscriptionId);
}
public function scopeForMeter($query, int $meterId)
{
return $query->where('meter_id', $meterId);
}
public function scopeForWorkspace($query, int $workspaceId)
{
return $query->where('workspace_id', $workspaceId);
}
public function scopeSince($query, $date)
{
return $query->where('event_at', '>=', $date);
}
public function scopeBetween($query, $start, $end)
{
return $query->whereBetween('event_at', [$start, $end]);
}
// Helpers
/**
* Generate a unique idempotency key.
*/
public static function generateIdempotencyKey(): string
{
return Str::uuid()->toString();
}
/**
* Check if an event with this idempotency key already exists.
*/
public static function existsByIdempotencyKey(string $key): bool
{
return static::where('idempotency_key', $key)->exists();
}
/**
* Create event with idempotency protection.
*
* Returns null if duplicate idempotency key.
*/
public static function createWithIdempotency(array $attributes): ?self
{
$key = $attributes['idempotency_key'] ?? null;
if ($key && static::existsByIdempotencyKey($key)) {
return null;
}
return static::create($attributes);
}
/**
* Get total quantity for a subscription + meter in a period.
*/
public static function getTotalQuantity(
int $subscriptionId,
int $meterId,
$periodStart,
$periodEnd
): int {
return (int) static::where('subscription_id', $subscriptionId)
->where('meter_id', $meterId)
->whereBetween('event_at', [$periodStart, $periodEnd])
->sum('quantity');
}
}