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

177 lines
4.4 KiB
PHP

<?php
namespace Core\Mod\Commerce\Models;
use Carbon\Carbon;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
/**
* SubscriptionUsage model - aggregated usage per subscription per billing period.
*
* @property int $id
* @property int $subscription_id
* @property int $meter_id
* @property int $quantity
* @property \Carbon\Carbon $period_start
* @property \Carbon\Carbon $period_end
* @property string|null $stripe_usage_record_id
* @property \Carbon\Carbon|null $synced_at
* @property bool $billed
* @property int|null $invoice_item_id
* @property array|null $metadata
*/
class SubscriptionUsage extends Model
{
protected $table = 'commerce_subscription_usage';
protected $fillable = [
'subscription_id',
'meter_id',
'quantity',
'period_start',
'period_end',
'stripe_usage_record_id',
'synced_at',
'billed',
'invoice_item_id',
'metadata',
];
protected $casts = [
'quantity' => 'integer',
'period_start' => 'datetime',
'period_end' => 'datetime',
'synced_at' => 'datetime',
'billed' => 'boolean',
'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 invoiceItem(): BelongsTo
{
return $this->belongsTo(InvoiceItem::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 scopeInPeriod($query, Carbon $start, Carbon $end)
{
return $query->where('period_start', '>=', $start)
->where('period_end', '<=', $end);
}
public function scopeCurrentPeriod($query, Subscription $subscription)
{
return $query->where('period_start', '>=', $subscription->current_period_start)
->where('period_end', '<=', $subscription->current_period_end);
}
public function scopeUnbilled($query)
{
return $query->where('billed', false);
}
public function scopeUnsynced($query)
{
return $query->whereNull('synced_at');
}
// Helpers
/**
* Check if this usage record is in the current billing period.
*/
public function isCurrentPeriod(): bool
{
$now = now();
return $now->between($this->period_start, $this->period_end);
}
/**
* Calculate the charge for this usage.
*/
public function calculateCharge(): float
{
return $this->meter->calculateCharge($this->quantity);
}
/**
* Add quantity to this usage record.
*/
public function addQuantity(int $quantity): self
{
$this->increment('quantity', $quantity);
return $this->fresh();
}
/**
* Mark as synced with Stripe.
*/
public function markSynced(?string $stripeUsageRecordId = null): void
{
$this->update([
'synced_at' => now(),
'stripe_usage_record_id' => $stripeUsageRecordId,
]);
}
/**
* Mark as billed.
*/
public function markBilled(?int $invoiceItemId = null): void
{
$this->update([
'billed' => true,
'invoice_item_id' => $invoiceItemId,
]);
}
/**
* Get or create usage record for current period.
*/
public static function getOrCreateForCurrentPeriod(
Subscription $subscription,
UsageMeter $meter
): self {
$record = static::where('subscription_id', $subscription->id)
->where('meter_id', $meter->id)
->where('period_start', $subscription->current_period_start)
->first();
if (! $record) {
$record = static::create([
'subscription_id' => $subscription->id,
'meter_id' => $meter->id,
'quantity' => 0,
'period_start' => $subscription->current_period_start,
'period_end' => $subscription->current_period_end,
]);
}
return $record;
}
}