feat: stripe

This commit is contained in:
2026-05-27 10:15:21 +02:00
parent 79e020ffe3
commit a99d027771
29 changed files with 1160 additions and 4 deletions
+142
View File
@@ -0,0 +1,142 @@
<?php
namespace App\Services;
use App\Enums\CreditLedgerEntryType;
use App\Models\CreditLedgerEntry;
use App\Models\CreditProduct;
use App\Models\User;
use Illuminate\Support\Facades\DB;
class AiCreditService
{
public function consume(User $user, int $credits = 1, ?string $source = null): bool
{
return DB::transaction(function () use ($credits, $source, $user): bool {
$lockedUser = User::query()
->whereKey($user->getKey())
->lockForUpdate()
->firstOrFail();
if ($lockedUser->ai_credits_balance < $credits) {
return false;
}
$lockedUser->decrement('ai_credits_balance', $credits);
$lockedUser->refresh();
$this->record(
user: $lockedUser,
type: CreditLedgerEntryType::USAGE,
credits: -$credits,
source: $source,
);
$user->forceFill([
'ai_credits_balance' => $lockedUser->ai_credits_balance,
]);
return true;
});
}
public function refund(User $user, int $credits = 1, ?string $source = null): CreditLedgerEntry
{
return $this->grant(
user: $user,
credits: $credits,
type: CreditLedgerEntryType::REFUND,
source: $source,
);
}
/**
* @param array<string, mixed>|null $metadata
*/
public function grant(
User $user,
int $credits,
CreditLedgerEntryType $type,
?CreditProduct $product = null,
?string $source = null,
?string $stripeCheckoutSessionId = null,
?string $stripeInvoiceId = null,
?string $stripePaymentIntentId = null,
?string $stripeSubscriptionId = null,
?array $metadata = null,
): ?CreditLedgerEntry {
return DB::transaction(function () use (
$credits,
$metadata,
$product,
$source,
$stripeCheckoutSessionId,
$stripeInvoiceId,
$stripePaymentIntentId,
$stripeSubscriptionId,
$type,
$user,
): ?CreditLedgerEntry {
if ($source && CreditLedgerEntry::query()->where('source', $source)->exists()) {
return null;
}
$lockedUser = User::query()
->whereKey($user->getKey())
->lockForUpdate()
->firstOrFail();
$lockedUser->increment('ai_credits_balance', $credits);
$lockedUser->refresh();
$entry = $this->record(
user: $lockedUser,
type: $type,
credits: $credits,
product: $product,
source: $source,
stripeCheckoutSessionId: $stripeCheckoutSessionId,
stripeInvoiceId: $stripeInvoiceId,
stripePaymentIntentId: $stripePaymentIntentId,
stripeSubscriptionId: $stripeSubscriptionId,
metadata: $metadata,
);
$user->forceFill([
'ai_credits_balance' => $lockedUser->ai_credits_balance,
]);
return $entry;
});
}
/**
* @param array<string, mixed>|null $metadata
*/
private function record(
User $user,
CreditLedgerEntryType $type,
int $credits,
?CreditProduct $product = null,
?string $source = null,
?string $stripeCheckoutSessionId = null,
?string $stripeInvoiceId = null,
?string $stripePaymentIntentId = null,
?string $stripeSubscriptionId = null,
?array $metadata = null,
): CreditLedgerEntry {
return CreditLedgerEntry::create([
'user_id' => $user->getKey(),
'credit_product_id' => $product?->getKey(),
'type' => $type,
'credits' => $credits,
'balance_after' => $user->ai_credits_balance,
'source' => $source,
'stripe_checkout_session_id' => $stripeCheckoutSessionId,
'stripe_invoice_id' => $stripeInvoiceId,
'stripe_payment_intent_id' => $stripePaymentIntentId,
'stripe_subscription_id' => $stripeSubscriptionId,
'metadata' => $metadata,
]);
}
}
@@ -0,0 +1,58 @@
<?php
namespace App\Services;
use App\Enums\CreditProductType;
use App\Models\CreditProduct;
use Laravel\Cashier\Cashier;
class StripeCreditProductSyncer
{
public function sync(CreditProduct $product): CreditProduct
{
$stripe = Cashier::stripe();
$stripeProductId = $product->stripe_product_id;
if (! $stripeProductId) {
$stripeProduct = $stripe->products->create([
'name' => $product->name,
'description' => $product->description,
'metadata' => [
'credit_product_id' => $product->getKey(),
'credits' => (string) $product->credits,
'type' => $product->type->value,
],
]);
$stripeProductId = $stripeProduct->id;
}
if (! $product->stripe_price_id) {
$priceData = [
'product' => $stripeProductId,
'unit_amount' => $product->amount,
'currency' => strtolower($product->currency),
'metadata' => [
'credit_product_id' => $product->getKey(),
'credits' => (string) $product->credits,
'type' => $product->type->value,
],
];
if ($product->type === CreditProductType::MONTHLY) {
$priceData['recurring'] = [
'interval' => 'month',
];
}
$stripePrice = $stripe->prices->create($priceData);
}
$product->forceFill([
'stripe_product_id' => $stripeProductId,
'stripe_price_id' => $product->stripe_price_id ?: $stripePrice->id,
])->save();
return $product;
}
}