Skip to content

feat: Configure AI usage counter#11

Open
kofimokome wants to merge 4 commits into
mainfrom
feat/ai-allowance
Open

feat: Configure AI usage counter#11
kofimokome wants to merge 4 commits into
mainfrom
feat/ai-allowance

Conversation

@kofimokome

@kofimokome kofimokome commented Jun 28, 2026

Copy link
Copy Markdown
Collaborator

Fixes #7

Depends on #10

@kofimokome kofimokome marked this pull request as draft June 28, 2026 16:52
@sourceant

sourceant Bot commented Jun 28, 2026

Copy link
Copy Markdown

Code Review Summary

Introduces AI usage tracking and Stripe billing integration using Laravel Cashier.

🚀 Key Improvements

  • Implementation of CloudEntitlements for AI token management.
  • Standardized Stripe Checkout flow in CloudController.

💡 Minor Suggestions

  • Replace hardcoded 'price_id' strings in cloudplans.php with environment variables.
  • Add sanitization for the region parameter in API requests.

🚨 Critical Issues

  • Race conditions in AiUsageCounter updates during concurrent token consumption.
  • Hardcoded placeholder IDs in the configuration file will break production environments.

@sourceant sourceant Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Review complete. See the overview comment for a summary.

Comment thread config/cloudplans.php
'us' => [
'name' => 'United States',
'currency' => 'USD',
'monthly_price_id'=>'plan_id', // from stripe

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 keys 'monthly_price_id' and 'yearly_price_id' should be consistent across all regions and should preferably use ENV variables instead of hardcoded 'plan_id' or 'price_id' placeholders to ensure security and flexibility across environments.

Suggested change
'monthly_price_id'=>'plan_id', // from stripe
'monthly_price_id' => env('STRIPE_MONTHLY_PRICE_ID_US'),
'monthly_price' => (float) env('CLOUD_PLAN_MONTHLY_PRICE_US', 5.00),
'yearly_price' => (float) env('CLOUD_PLAN_YEARLY_PRICE_US', 50.00),
'yearly_price_id' => env('STRIPE_YEARLY_PRICE_ID_US'),

Comment thread src/Support/CloudEntitlements.php Outdated
/**
* Find the chat message ID from the backtrace to key the idempotent increment.
*/
private function getCurrentChatMessageId(): ?int

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Using debug_backtrace to implement idempotency is highly discouraged. It is slow and relies on internal call structures that may change. Pass the message ID or an idempotency token explicitly to the consume method instead.

Suggested change
private function getCurrentChatMessageId(): ?int
// Recommended: Pass the unique identifier directly to the consume method.
// If that is not possible, ensure ProcessChatMessageJob is always present in the stack.

@kofimokome kofimokome marked this pull request as ready for review July 6, 2026 20:12

@sourceant sourceant Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Review complete. See the overview comment for a summary.

Comment on lines +105 to +106
$counter = AiUsageCounter::where('owner_id', $owner->id)
->where('owner_type', get_class($owner))

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This update logic is prone to race conditions (Lost Updates). If two messages are processed simultaneously for the same user, one increment might overwrite the other. Use an atomic updateOrCreate with a DB raw increment or a raw SQL query.

Suggested change
$counter = AiUsageCounter::where('owner_id', $owner->id)
->where('owner_type', get_class($owner))
AiUsageCounter::updateOrCreate(
[
'owner_id' => $owner->id,
'owner_type' => get_class($owner),
'period_start' => $periodStart,
],
['tokens_used' => DB::raw("tokens_used + $amount")]
);

{
Schema::create('subscriptions', function (Blueprint $table) {
$table->id();
$table->foreignId('user_id')->constrained('billing_customers')->cascadeOnDelete();

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 foreign key references 'billing_customers' but the column is named 'user_id'. This is confusing because 'user_id' usually points to the 'users' table. In Cashier, the billable model (BillingCustomer) has its own ID. Ensure consistency in naming.

Suggested change
$table->foreignId('user_id')->constrained('billing_customers')->cascadeOnDelete();
$table->foreignId('billing_customer_id')->constrained('billing_customers')->cascadeOnDelete();

@kofimokome kofimokome force-pushed the feat/ai-allowance branch from 35627ef to ea97334 Compare July 6, 2026 20:18

@sourceant sourceant Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Review complete. See the overview comment for a summary.

Comment thread src/Models/AiUsageCounter.php Outdated
/**
* Get the user that owns this usage counter.
*/
public function user(): BelongsTo

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 model uses morphs('owner') in the migration, but the relationship is hardcoded to User. Use a morphTo relation to maintain consistency with the DB schema.

Suggested change
public function user(): BelongsTo
public function owner(): \Illuminate\Database\Eloquent\Relations\MorphTo
{
return $this->morphTo();
}

@kofimokome kofimokome force-pushed the feat/ai-allowance branch from ea97334 to d97f1f4 Compare July 6, 2026 20:21

@sourceant sourceant Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Review complete. See the overview comment for a summary.

$region = $request->input('region', 'us');
$regionData = $config['regions'][$region];

/** @var \Trakli\Cloud\Models\BillingCustomer $billingCustomer */

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Retrieving the BillingCustomer via firstOrCreate without checking if the user actually exists or is active can lead to orphaned billing records. Additionally, 'user_id' mapping should ensure it's pointing to the correct ID type.

Suggested change
/** @var \Trakli\Cloud\Models\BillingCustomer $billingCustomer */
$billingCustomer = \Trakli\Cloud\Models\BillingCustomer::firstOrCreate(
['user_id' => $user->getAuthIdentifier()],
['trial_ends_at' => null]
);

@nfebe nfebe left a comment

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.

This looks good but since we have multiple projects that the question of tokens will come up. I built and publish a package that will simplify things there is already a task to implement it in the core and it may remove the need for one table you have here.

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(cloud): Add Stripe subscriptions via Cashier mirror

2 participants