Skip to content
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions config/cloudplans.php
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@
'free' => [
'id' => 'free',
'name' => 'Free',
'token_allowance' => (int) env('CLOUD_PLAN_FREE_TOKEN_ALLOWANCE', 0),
'interval' => 'lifetime',
'features' => [
'Up to 3 wallets',
Expand All @@ -83,6 +84,7 @@
'monthly' => [
'id' => 'monthly',
'name' => 'Monthly',
'token_allowance' => (int) env('CLOUD_PLAN_MONTHLY_TOKEN_ALLOWANCE', 50000),
'interval' => 'month',
'features' => [
'Unlimited categories and wallets',
Expand All @@ -97,6 +99,7 @@
],
'yearly' => [
'id' => 'yearly',
'token_allowance' => (int) env('CLOUD_PLAN_YEARLY_TOKEN_ALLOWANCE', 50000),
'name' => 'Yearly',
'interval' => 'year',
'features' => [
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class () extends Migration {
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('ai_usage_counters', function (Blueprint $table) {
$table->id();
$table->morphs('owner');
$table->timestamp('period_start');
$table->integer('tokens_used');

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you add this to the webservice and see how we can eliminate this table?

https://github.com/whilesmartphp/eloquent-agent-metrics

Was this solved?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm still working on this and the other open pr.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So I already integrated the metrics tracking in the core : https://github.com/trakli/webservice/blob/dev/composer.json#L26

So you just need to used it and remove the custom tracking tables


$table->timestamps();
});
}

/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('ai_usage_counters');
}
};
46 changes: 46 additions & 0 deletions src/Models/AiUsageCounter.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
<?php

namespace Trakli\Cloud\Models;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\MorphTo;

class AiUsageCounter extends Model
{
/**
* The table associated with the model.
*
* @var string
*/
protected $table = 'ai_usage_counters';

/**
* The attributes that are mass assignable.
*
* @var array<int, string>
*/
protected $fillable = [
'owner_id',
'owner_type',
'period_start',
'tokens_used',
];

/**
* The attributes that should be cast.
*
* @var array<string, string>
*/
protected $casts = [
'period_start' => 'datetime',
];

/**
* Get the user that owns this usage counter.
*/
public function owner(): MorphTo
{
return $this->morphTo();
}
}
129 changes: 129 additions & 0 deletions src/Support/CloudEntitlements.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
<?php

namespace Trakli\Cloud\Support;

use App\Contracts\Entitlements;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Carbon;
use Trakli\Cloud\Models\AiUsageCounter;

class CloudEntitlements implements Entitlements
{
/**
* Determine if the owner is allowed to use a given feature.
*/
public function allows(?Model $owner, string $feature): bool

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The allows method is currently hardcoded to return true, which bypasses all plan-based feature restrictions. This should delegate to the check method to ensure that entitlements are properly enforced across the application.

Suggested change
public function allows(?Model $owner, string $feature): bool
public function allows(?Model $owner, string $feature): bool
{
return $this->check($owner, $feature)->allowed();
}

{
return true;
}

/**
* Get the limit for a given key.
*/
public function limit(?Model $owner, string $key): ?int
{
return null;
}

/**
* Get the remaining token allowance for the current period.
*/
public function remaining(?Model $owner, string $meter): int|float
{
if ($meter !== 'ai_tokens') {
return INF;
}

// If in freemode, return unlimited tokens
if (config('cloudplans.freemode_enabled', false)) {
return INF;
}

if (!$owner) {
return 0;
}

// Get owner's current plan via Cashier
$planCode = $this->getPlanCode($owner);
if ($planCode === 'free') {
Comment thread
kofimokome marked this conversation as resolved.
Outdated
return INF;
}

$plan = config("cloudplans.plans.{$planCode}");
if (!$plan) {
return 0;
}

$allowance = $plan['token_allowance'] ?? 0;
if ($allowance <= 0) {
return 0;
}

$periodStart = Carbon::now()->startOfMonth();
$used = AiUsageCounter::where('owner_id', $owner->id)
->where('owner_type', get_class($owner))
->where('period_start', $periodStart)
->value('tokens_used') ?? 0;

return max(0, $allowance - $used);
}

/**
* Consume a given amount of tokens for the current period.
*/
public function consume(?Model $owner, string $meter, int $amount): void
{
if ($meter !== 'ai_tokens' || $amount <= 0 || !$owner) {
return;
}

if (config('cloudplans.freemode_enabled', false)) {
return;
}

$planCode = $this->getPlanCode($owner);
if ($planCode === 'free') {
return;
}

$periodStart = Carbon::now()->startOfMonth();

$counter = AiUsageCounter::where('owner_id', $owner->id)
->where('owner_type', get_class($owner))
Comment thread
kofimokome marked this conversation as resolved.
Outdated
->where('period_start', $periodStart)
->first();

if ($counter) {
$counter->increment('tokens_used', $amount);
} else {
AiUsageCounter::create([
'owner_id' => $owner->id,
'owner_type' => get_class($owner),
'period_start' => $periodStart,
'tokens_used' => $amount,
]);
}
}

/**
* Get the active plan code for the owner via Cashier.
*/
private function getPlanCode(Model $owner): string
{
/** @var \Trakli\Cloud\Models\BillingCustomer|null $billingCustomer */
$billingCustomer = \Trakli\Cloud\Models\BillingCustomer::where('user_id', $owner->getKey())->first();
if ($billingCustomer) {
if ($billingCustomer->subscribed('monthly')) {
return 'monthly';
}
if ($billingCustomer->subscribed('yearly')) {
return 'yearly';
}
return 'free';
}

return 'free';
}
}