feat: stripe
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user