'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'); } }