feat: Configure AI usage counter#11
Conversation
# Conflicts: # src/CloudServiceProvider.php
Code Review SummaryIntroduces AI usage tracking and Stripe billing integration using Laravel Cashier. 🚀 Key Improvements
💡 Minor Suggestions
🚨 Critical Issues
|
| 'us' => [ | ||
| 'name' => 'United States', | ||
| 'currency' => 'USD', | ||
| 'monthly_price_id'=>'plan_id', // from stripe |
There was a problem hiding this comment.
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.
| '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'), |
| /** | ||
| * Find the chat message ID from the backtrace to key the idempotent increment. | ||
| */ | ||
| private function getCurrentChatMessageId(): ?int |
There was a problem hiding this comment.
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.
| 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. |
| $counter = AiUsageCounter::where('owner_id', $owner->id) | ||
| ->where('owner_type', get_class($owner)) |
There was a problem hiding this comment.
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.
| $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(); |
There was a problem hiding this comment.
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.
| $table->foreignId('user_id')->constrained('billing_customers')->cascadeOnDelete(); | |
| $table->foreignId('billing_customer_id')->constrained('billing_customers')->cascadeOnDelete(); |
35627ef to
ea97334
Compare
| /** | ||
| * Get the user that owns this usage counter. | ||
| */ | ||
| public function user(): BelongsTo |
There was a problem hiding this comment.
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.
| public function user(): BelongsTo | |
| public function owner(): \Illuminate\Database\Eloquent\Relations\MorphTo | |
| { | |
| return $this->morphTo(); | |
| } |
ea97334 to
d97f1f4
Compare
| $region = $request->input('region', 'us'); | ||
| $regionData = $config['regions'][$region]; | ||
|
|
||
| /** @var \Trakli\Cloud\Models\BillingCustomer $billingCustomer */ |
There was a problem hiding this comment.
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.
| /** @var \Trakli\Cloud\Models\BillingCustomer $billingCustomer */ | |
| $billingCustomer = \Trakli\Cloud\Models\BillingCustomer::firstOrCreate( | |
| ['user_id' => $user->getAuthIdentifier()], | |
| ['trial_ends_at' => null] | |
| ); |
nfebe
left a comment
There was a problem hiding this comment.
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
Fixes #7
Depends on #10