feat: clean credits users
This commit is contained in:
@@ -1,38 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Enums;
|
||||
|
||||
use Filament\Support\Contracts\HasColor;
|
||||
use Filament\Support\Contracts\HasLabel;
|
||||
|
||||
enum CreditLedgerEntryType: string implements HasColor, HasLabel
|
||||
{
|
||||
case FREE_GRANT = 'free_grant';
|
||||
case PURCHASE = 'purchase';
|
||||
case SUBSCRIPTION_RENEWAL = 'subscription_renewal';
|
||||
case USAGE = 'usage';
|
||||
case REFUND = 'refund';
|
||||
case ADJUSTMENT = 'adjustment';
|
||||
|
||||
public function getLabel(): string
|
||||
{
|
||||
return match ($this) {
|
||||
self::FREE_GRANT => __('enums.credit_ledger_entry_type.free_grant'),
|
||||
self::PURCHASE => __('enums.credit_ledger_entry_type.purchase'),
|
||||
self::SUBSCRIPTION_RENEWAL => __('enums.credit_ledger_entry_type.subscription_renewal'),
|
||||
self::USAGE => __('enums.credit_ledger_entry_type.usage'),
|
||||
self::REFUND => __('enums.credit_ledger_entry_type.refund'),
|
||||
self::ADJUSTMENT => __('enums.credit_ledger_entry_type.adjustment'),
|
||||
};
|
||||
}
|
||||
|
||||
public function getColor(): string
|
||||
{
|
||||
return match ($this) {
|
||||
self::FREE_GRANT, self::PURCHASE, self::SUBSCRIPTION_RENEWAL => 'success',
|
||||
self::USAGE => 'warning',
|
||||
self::REFUND => 'info',
|
||||
self::ADJUSTMENT => 'gray',
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Enums;
|
||||
|
||||
use Filament\Support\Contracts\HasColor;
|
||||
use Filament\Support\Contracts\HasLabel;
|
||||
|
||||
enum CreditProductType: string implements HasColor, HasLabel
|
||||
{
|
||||
case MONTHLY = 'monthly';
|
||||
case ONE_TIME = 'one_time';
|
||||
|
||||
public function getLabel(): string
|
||||
{
|
||||
return match ($this) {
|
||||
self::MONTHLY => __('enums.credit_product_type.monthly'),
|
||||
self::ONE_TIME => __('enums.credit_product_type.one_time'),
|
||||
};
|
||||
}
|
||||
|
||||
public function getColor(): string
|
||||
{
|
||||
return match ($this) {
|
||||
self::MONTHLY => 'info',
|
||||
self::ONE_TIME => 'success',
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -9,7 +9,6 @@ enum PaymentTransactionStatus: string implements HasColor, HasLabel
|
||||
{
|
||||
case PENDING = 'pending';
|
||||
case PAID = 'paid';
|
||||
case CREDITED = 'credited';
|
||||
case FAILED = 'failed';
|
||||
case IGNORED = 'ignored';
|
||||
case ERROR = 'error';
|
||||
@@ -19,7 +18,6 @@ enum PaymentTransactionStatus: string implements HasColor, HasLabel
|
||||
return match ($this) {
|
||||
self::PENDING => __('enums.payment_transaction_status.pending'),
|
||||
self::PAID => __('enums.payment_transaction_status.paid'),
|
||||
self::CREDITED => __('enums.payment_transaction_status.credited'),
|
||||
self::FAILED => __('enums.payment_transaction_status.failed'),
|
||||
self::IGNORED => __('enums.payment_transaction_status.ignored'),
|
||||
self::ERROR => __('enums.payment_transaction_status.error'),
|
||||
@@ -30,8 +28,7 @@ enum PaymentTransactionStatus: string implements HasColor, HasLabel
|
||||
{
|
||||
return match ($this) {
|
||||
self::PENDING => 'gray',
|
||||
self::PAID => 'info',
|
||||
self::CREDITED => 'success',
|
||||
self::PAID => 'success',
|
||||
self::FAILED, self::ERROR => 'danger',
|
||||
self::IGNORED => 'warning',
|
||||
};
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\BillingProducts;
|
||||
|
||||
use App\Filament\Resources\BillingProducts\Pages\CreateBillingProduct;
|
||||
use App\Filament\Resources\BillingProducts\Pages\EditBillingProduct;
|
||||
use App\Filament\Resources\BillingProducts\Pages\ListBillingProducts;
|
||||
use App\Filament\Resources\BillingProducts\Schemas\BillingProductForm;
|
||||
use App\Filament\Resources\BillingProducts\Tables\BillingProductsTable;
|
||||
use App\Models\BillingProduct;
|
||||
use App\Services\StripeBillingProductSyncer;
|
||||
use BackedEnum;
|
||||
use Filament\Actions\Action;
|
||||
use Filament\Notifications\Notification;
|
||||
use Filament\Resources\Resource;
|
||||
use Filament\Schemas\Schema;
|
||||
use Filament\Support\Icons\Heroicon;
|
||||
use Filament\Tables\Table;
|
||||
|
||||
class BillingProductResource extends Resource
|
||||
{
|
||||
protected static ?string $model = BillingProduct::class;
|
||||
|
||||
protected static string|BackedEnum|null $navigationIcon = Heroicon::OutlinedCreditCard;
|
||||
|
||||
protected static ?string $recordTitleAttribute = 'name';
|
||||
|
||||
public static function getNavigationLabel(): string
|
||||
{
|
||||
return __('admin.billing_products.navigation.label');
|
||||
}
|
||||
|
||||
public static function getModelLabel(): string
|
||||
{
|
||||
return __('admin.billing_products.navigation.singular');
|
||||
}
|
||||
|
||||
public static function getPluralModelLabel(): string
|
||||
{
|
||||
return __('admin.billing_products.navigation.plural');
|
||||
}
|
||||
|
||||
public static function form(Schema $schema): Schema
|
||||
{
|
||||
return BillingProductForm::configure($schema);
|
||||
}
|
||||
|
||||
public static function table(Table $table): Table
|
||||
{
|
||||
return BillingProductsTable::configure($table);
|
||||
}
|
||||
|
||||
public static function syncStripeAction(): Action
|
||||
{
|
||||
return Action::make('syncStripe')
|
||||
->label(__('admin.billing_products.actions.sync_stripe'))
|
||||
->icon(Heroicon::OutlinedArrowPath)
|
||||
->visible(fn (BillingProduct $record): bool => blank($record->stripe_price_id) && $record->purpose !== null)
|
||||
->requiresConfirmation()
|
||||
->action(function (BillingProduct $record): void {
|
||||
app(StripeBillingProductSyncer::class)->sync($record);
|
||||
|
||||
Notification::make()
|
||||
->title(__('admin.billing_products.actions.sync_stripe_success'))
|
||||
->success()
|
||||
->send();
|
||||
});
|
||||
}
|
||||
|
||||
public static function getPages(): array
|
||||
{
|
||||
return [
|
||||
'index' => ListBillingProducts::route('/'),
|
||||
'create' => CreateBillingProduct::route('/create'),
|
||||
'edit' => EditBillingProduct::route('/{record}/edit'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\BillingProducts\Pages;
|
||||
|
||||
use App\Filament\Resources\BillingProducts\BillingProductResource;
|
||||
use App\Services\StripeBillingProductSyncer;
|
||||
use Filament\Resources\Pages\CreateRecord;
|
||||
|
||||
class CreateBillingProduct extends CreateRecord
|
||||
{
|
||||
protected static string $resource = BillingProductResource::class;
|
||||
|
||||
protected function afterCreate(): void
|
||||
{
|
||||
app(StripeBillingProductSyncer::class)->sync($this->record);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\BillingProducts\Pages;
|
||||
|
||||
use App\Filament\Resources\BillingProducts\BillingProductResource;
|
||||
use App\Services\StripeBillingProductSyncer;
|
||||
use Filament\Actions\DeleteAction;
|
||||
use Filament\Resources\Pages\EditRecord;
|
||||
|
||||
class EditBillingProduct extends EditRecord
|
||||
{
|
||||
protected static string $resource = BillingProductResource::class;
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
BillingProductResource::syncStripeAction(),
|
||||
DeleteAction::make(),
|
||||
];
|
||||
}
|
||||
|
||||
protected function afterSave(): void
|
||||
{
|
||||
app(StripeBillingProductSyncer::class)->sync($this->record);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\BillingProducts\Pages;
|
||||
|
||||
use App\Filament\Resources\BillingProducts\BillingProductResource;
|
||||
use Filament\Actions\CreateAction;
|
||||
use Filament\Resources\Pages\ListRecords;
|
||||
|
||||
class ListBillingProducts extends ListRecords
|
||||
{
|
||||
protected static string $resource = BillingProductResource::class;
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
CreateAction::make(),
|
||||
];
|
||||
}
|
||||
}
|
||||
+14
-20
@@ -1,10 +1,8 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\CreditProducts\Schemas;
|
||||
namespace App\Filament\Resources\BillingProducts\Schemas;
|
||||
|
||||
use App\Enums\BillingProductPurpose;
|
||||
use App\Enums\CreditProductType;
|
||||
use Filament\Forms\Components\Hidden;
|
||||
use Filament\Forms\Components\Select;
|
||||
use Filament\Forms\Components\Textarea;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
@@ -12,60 +10,56 @@ use Filament\Forms\Components\Toggle;
|
||||
use Filament\Schemas\Components\Section;
|
||||
use Filament\Schemas\Schema;
|
||||
|
||||
class CreditProductForm
|
||||
class BillingProductForm
|
||||
{
|
||||
public static function configure(Schema $schema): Schema
|
||||
{
|
||||
return $schema
|
||||
->components([
|
||||
Section::make(__('admin.credit_products.sections.product'))
|
||||
Section::make(__('admin.billing_products.sections.product'))
|
||||
->columns(2)
|
||||
->schema([
|
||||
TextInput::make('name')
|
||||
->label(__('admin.credit_products.fields.name'))
|
||||
->label(__('admin.billing_products.fields.name'))
|
||||
->required()
|
||||
->maxLength(255),
|
||||
Select::make('purpose')
|
||||
->label(__('admin.credit_products.fields.purpose'))
|
||||
->label(__('admin.billing_products.fields.purpose'))
|
||||
->options(BillingProductPurpose::class)
|
||||
->unique(ignoreRecord: true)
|
||||
->required(),
|
||||
Hidden::make('type')
|
||||
->default(CreditProductType::ONE_TIME->value),
|
||||
Hidden::make('credits')
|
||||
->default(0),
|
||||
TextInput::make('amount')
|
||||
->label(__('admin.credit_products.fields.amount'))
|
||||
->helperText(__('admin.credit_products.helpers.amount'))
|
||||
->label(__('admin.billing_products.fields.amount'))
|
||||
->helperText(__('admin.billing_products.helpers.amount'))
|
||||
->integer()
|
||||
->minValue(1)
|
||||
->required(),
|
||||
TextInput::make('currency')
|
||||
->label(__('admin.credit_products.fields.currency'))
|
||||
->label(__('admin.billing_products.fields.currency'))
|
||||
->default('eur')
|
||||
->required()
|
||||
->maxLength(3),
|
||||
TextInput::make('sort_order')
|
||||
->label(__('admin.credit_products.fields.sort_order'))
|
||||
->label(__('admin.billing_products.fields.sort_order'))
|
||||
->integer()
|
||||
->default(0)
|
||||
->required(),
|
||||
Textarea::make('description')
|
||||
->label(__('admin.credit_products.fields.description'))
|
||||
->label(__('admin.billing_products.fields.description'))
|
||||
->rows(3)
|
||||
->columnSpanFull(),
|
||||
Toggle::make('is_active')
|
||||
->label(__('admin.credit_products.fields.is_active'))
|
||||
->label(__('admin.billing_products.fields.is_active'))
|
||||
->default(true),
|
||||
]),
|
||||
Section::make(__('admin.credit_products.sections.stripe'))
|
||||
Section::make(__('admin.billing_products.sections.stripe'))
|
||||
->columns(2)
|
||||
->schema([
|
||||
TextInput::make('stripe_product_id')
|
||||
->label(__('admin.credit_products.fields.stripe_product_id'))
|
||||
->label(__('admin.billing_products.fields.stripe_product_id'))
|
||||
->maxLength(255),
|
||||
TextInput::make('stripe_price_id')
|
||||
->label(__('admin.credit_products.fields.stripe_price_id'))
|
||||
->label(__('admin.billing_products.fields.stripe_price_id'))
|
||||
->unique(ignoreRecord: true)
|
||||
->maxLength(255),
|
||||
]),
|
||||
+13
-13
@@ -1,9 +1,9 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\CreditProducts\Tables;
|
||||
namespace App\Filament\Resources\BillingProducts\Tables;
|
||||
|
||||
use App\Enums\BillingProductPurpose;
|
||||
use App\Filament\Resources\CreditProducts\CreditProductResource;
|
||||
use App\Filament\Resources\BillingProducts\BillingProductResource;
|
||||
use Filament\Actions\BulkActionGroup;
|
||||
use Filament\Actions\DeleteBulkAction;
|
||||
use Filament\Actions\EditAction;
|
||||
@@ -13,7 +13,7 @@ use Filament\Tables\Filters\SelectFilter;
|
||||
use Filament\Tables\Filters\TernaryFilter;
|
||||
use Filament\Tables\Table;
|
||||
|
||||
class CreditProductsTable
|
||||
class BillingProductsTable
|
||||
{
|
||||
public static function configure(Table $table): Table
|
||||
{
|
||||
@@ -21,45 +21,45 @@ class CreditProductsTable
|
||||
->defaultSort('sort_order')
|
||||
->columns([
|
||||
TextColumn::make('name')
|
||||
->label(__('admin.credit_products.fields.name'))
|
||||
->label(__('admin.billing_products.fields.name'))
|
||||
->searchable()
|
||||
->sortable(),
|
||||
TextColumn::make('purpose')
|
||||
->label(__('admin.credit_products.fields.purpose'))
|
||||
->label(__('admin.billing_products.fields.purpose'))
|
||||
->badge()
|
||||
->sortable(),
|
||||
TextColumn::make('amount')
|
||||
->label(__('admin.credit_products.fields.amount'))
|
||||
->label(__('admin.billing_products.fields.amount'))
|
||||
->money(fn ($record): string => $record->currency, divideBy: 100)
|
||||
->sortable(),
|
||||
TextColumn::make('stripe_price_id')
|
||||
->label(__('admin.credit_products.fields.stripe_price_id'))
|
||||
->label(__('admin.billing_products.fields.stripe_price_id'))
|
||||
->copyable()
|
||||
->searchable()
|
||||
->toggleable(),
|
||||
IconColumn::make('is_active')
|
||||
->label(__('admin.credit_products.fields.is_active'))
|
||||
->label(__('admin.billing_products.fields.is_active'))
|
||||
->boolean()
|
||||
->sortable(),
|
||||
TextColumn::make('sort_order')
|
||||
->label(__('admin.credit_products.fields.sort_order'))
|
||||
->label(__('admin.billing_products.fields.sort_order'))
|
||||
->sortable()
|
||||
->toggleable(isToggledHiddenByDefault: true),
|
||||
TextColumn::make('created_at')
|
||||
->label(__('admin.credit_products.fields.created_at'))
|
||||
->label(__('admin.billing_products.fields.created_at'))
|
||||
->dateTime()
|
||||
->sortable()
|
||||
->toggleable(isToggledHiddenByDefault: true),
|
||||
])
|
||||
->filters([
|
||||
SelectFilter::make('purpose')
|
||||
->label(__('admin.credit_products.fields.purpose'))
|
||||
->label(__('admin.billing_products.fields.purpose'))
|
||||
->options(BillingProductPurpose::class),
|
||||
TernaryFilter::make('is_active')
|
||||
->label(__('admin.credit_products.fields.is_active')),
|
||||
->label(__('admin.billing_products.fields.is_active')),
|
||||
])
|
||||
->recordActions([
|
||||
CreditProductResource::syncStripeAction(),
|
||||
BillingProductResource::syncStripeAction(),
|
||||
EditAction::make(),
|
||||
])
|
||||
->toolbarActions([
|
||||
@@ -1,78 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\CreditProducts;
|
||||
|
||||
use App\Filament\Resources\CreditProducts\Pages\CreateCreditProduct;
|
||||
use App\Filament\Resources\CreditProducts\Pages\EditCreditProduct;
|
||||
use App\Filament\Resources\CreditProducts\Pages\ListCreditProducts;
|
||||
use App\Filament\Resources\CreditProducts\Schemas\CreditProductForm;
|
||||
use App\Filament\Resources\CreditProducts\Tables\CreditProductsTable;
|
||||
use App\Models\CreditProduct;
|
||||
use App\Services\StripeCreditProductSyncer;
|
||||
use BackedEnum;
|
||||
use Filament\Actions\Action;
|
||||
use Filament\Notifications\Notification;
|
||||
use Filament\Resources\Resource;
|
||||
use Filament\Schemas\Schema;
|
||||
use Filament\Support\Icons\Heroicon;
|
||||
use Filament\Tables\Table;
|
||||
|
||||
class CreditProductResource extends Resource
|
||||
{
|
||||
protected static ?string $model = CreditProduct::class;
|
||||
|
||||
protected static string|BackedEnum|null $navigationIcon = Heroicon::OutlinedCreditCard;
|
||||
|
||||
protected static ?string $recordTitleAttribute = 'name';
|
||||
|
||||
public static function getNavigationLabel(): string
|
||||
{
|
||||
return __('admin.credit_products.navigation.label');
|
||||
}
|
||||
|
||||
public static function getModelLabel(): string
|
||||
{
|
||||
return __('admin.credit_products.navigation.singular');
|
||||
}
|
||||
|
||||
public static function getPluralModelLabel(): string
|
||||
{
|
||||
return __('admin.credit_products.navigation.plural');
|
||||
}
|
||||
|
||||
public static function form(Schema $schema): Schema
|
||||
{
|
||||
return CreditProductForm::configure($schema);
|
||||
}
|
||||
|
||||
public static function table(Table $table): Table
|
||||
{
|
||||
return CreditProductsTable::configure($table);
|
||||
}
|
||||
|
||||
public static function syncStripeAction(): Action
|
||||
{
|
||||
return Action::make('syncStripe')
|
||||
->label(__('admin.credit_products.actions.sync_stripe'))
|
||||
->icon(Heroicon::OutlinedArrowPath)
|
||||
->visible(fn (CreditProduct $record): bool => blank($record->stripe_price_id) && $record->purpose !== null)
|
||||
->requiresConfirmation()
|
||||
->action(function (CreditProduct $record): void {
|
||||
app(StripeCreditProductSyncer::class)->sync($record);
|
||||
|
||||
Notification::make()
|
||||
->title(__('admin.credit_products.actions.sync_stripe_success'))
|
||||
->success()
|
||||
->send();
|
||||
});
|
||||
}
|
||||
|
||||
public static function getPages(): array
|
||||
{
|
||||
return [
|
||||
'index' => ListCreditProducts::route('/'),
|
||||
'create' => CreateCreditProduct::route('/create'),
|
||||
'edit' => EditCreditProduct::route('/{record}/edit'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\CreditProducts\Pages;
|
||||
|
||||
use App\Filament\Resources\CreditProducts\CreditProductResource;
|
||||
use Filament\Resources\Pages\CreateRecord;
|
||||
|
||||
class CreateCreditProduct extends CreateRecord
|
||||
{
|
||||
protected static string $resource = CreditProductResource::class;
|
||||
|
||||
protected function afterCreate(): void
|
||||
{
|
||||
app(\App\Services\StripeCreditProductSyncer::class)->sync($this->record);
|
||||
}
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\CreditProducts\Pages;
|
||||
|
||||
use App\Filament\Resources\CreditProducts\CreditProductResource;
|
||||
use Filament\Actions\DeleteAction;
|
||||
use Filament\Resources\Pages\EditRecord;
|
||||
|
||||
class EditCreditProduct extends EditRecord
|
||||
{
|
||||
protected static string $resource = CreditProductResource::class;
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
CreditProductResource::syncStripeAction(),
|
||||
DeleteAction::make(),
|
||||
];
|
||||
}
|
||||
|
||||
protected function afterSave(): void
|
||||
{
|
||||
app(\App\Services\StripeCreditProductSyncer::class)->sync($this->record);
|
||||
}
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\CreditProducts\Pages;
|
||||
|
||||
use App\Filament\Resources\CreditProducts\CreditProductResource;
|
||||
use Filament\Actions\CreateAction;
|
||||
use Filament\Resources\Pages\ListRecords;
|
||||
|
||||
class ListCreditProducts extends ListRecords
|
||||
{
|
||||
protected static string $resource = CreditProductResource::class;
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
CreateAction::make(),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -28,18 +28,13 @@ class PaymentTransactionInfolist
|
||||
->label(__('admin.payment_transactions.fields.user'))
|
||||
->placeholder(__('admin.payment_transactions.placeholders.empty'))
|
||||
->copyable(),
|
||||
TextEntry::make('creditProduct.name')
|
||||
->label(__('admin.payment_transactions.fields.credit_product'))
|
||||
TextEntry::make('billingProduct.name')
|
||||
->label(__('admin.payment_transactions.fields.billing_product'))
|
||||
->placeholder(__('admin.payment_transactions.placeholders.empty')),
|
||||
TextEntry::make('amount')
|
||||
->label(__('admin.payment_transactions.fields.amount'))
|
||||
->money(fn ($record): string => $record->currency ?: 'eur', divideBy: 100)
|
||||
->placeholder(__('admin.payment_transactions.placeholders.empty')),
|
||||
TextEntry::make('credits_expected')
|
||||
->label(__('admin.payment_transactions.fields.credits_expected'))
|
||||
->placeholder(__('admin.payment_transactions.placeholders.empty')),
|
||||
TextEntry::make('credits_granted')
|
||||
->label(__('admin.payment_transactions.fields.credits_granted')),
|
||||
TextEntry::make('processed_at')
|
||||
->label(__('admin.payment_transactions.fields.processed_at'))
|
||||
->dateTime()
|
||||
|
||||
@@ -32,8 +32,8 @@ class PaymentTransactionsTable
|
||||
->label(__('admin.payment_transactions.fields.user'))
|
||||
->searchable()
|
||||
->placeholder(__('admin.payment_transactions.placeholders.empty')),
|
||||
TextColumn::make('creditProduct.name')
|
||||
->label(__('admin.payment_transactions.fields.credit_product'))
|
||||
TextColumn::make('billingProduct.name')
|
||||
->label(__('admin.payment_transactions.fields.billing_product'))
|
||||
->searchable()
|
||||
->placeholder(__('admin.payment_transactions.placeholders.empty')),
|
||||
TextColumn::make('amount')
|
||||
@@ -41,15 +41,6 @@ class PaymentTransactionsTable
|
||||
->money(fn ($record): string => $record->currency ?: 'eur', divideBy: 100)
|
||||
->sortable()
|
||||
->placeholder(__('admin.payment_transactions.placeholders.empty')),
|
||||
TextColumn::make('credits_expected')
|
||||
->label(__('admin.payment_transactions.fields.credits_expected'))
|
||||
->numeric(decimalPlaces: 0)
|
||||
->sortable()
|
||||
->placeholder(__('admin.payment_transactions.placeholders.empty')),
|
||||
TextColumn::make('credits_granted')
|
||||
->label(__('admin.payment_transactions.fields.credits_granted'))
|
||||
->numeric(decimalPlaces: 0)
|
||||
->sortable(),
|
||||
TextColumn::make('stripe_event_type')
|
||||
->label(__('admin.payment_transactions.fields.stripe_event_type'))
|
||||
->toggleable(),
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Enums\BillingProductPurpose;
|
||||
use App\Http\Resources\CreditProductResource;
|
||||
use App\Models\CreditProduct;
|
||||
use App\Http\Resources\BillingProductResource;
|
||||
use App\Models\BillingProduct;
|
||||
use App\Services\MobileDeepLink;
|
||||
use App\Services\PaymentTransactionRecorder;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
@@ -16,7 +16,7 @@ class BillingController extends Controller
|
||||
{
|
||||
public function products(): AnonymousResourceCollection
|
||||
{
|
||||
$products = CreditProduct::query()
|
||||
$products = BillingProduct::query()
|
||||
->active()
|
||||
->whereNotNull('purpose')
|
||||
->whereNotNull('stripe_price_id')
|
||||
@@ -24,33 +24,33 @@ class BillingController extends Controller
|
||||
->orderBy('amount')
|
||||
->get();
|
||||
|
||||
return CreditProductResource::collection($products);
|
||||
return BillingProductResource::collection($products);
|
||||
}
|
||||
|
||||
public function checkout(
|
||||
Request $request,
|
||||
CreditProduct $creditProduct,
|
||||
BillingProduct $billingProduct,
|
||||
PaymentTransactionRecorder $transactions,
|
||||
): JsonResponse {
|
||||
abort_unless($creditProduct->is_active, 404);
|
||||
abort_unless($creditProduct->purpose !== null, 404);
|
||||
abort_unless(filled($creditProduct->stripe_price_id), 404);
|
||||
abort_unless($billingProduct->is_active, 404);
|
||||
abort_unless($billingProduct->purpose !== null, 404);
|
||||
abort_unless(filled($billingProduct->stripe_price_id), 404);
|
||||
|
||||
$user = $request->user();
|
||||
$this->ensureStripeCustomer($user);
|
||||
|
||||
$metadata = [
|
||||
'credit_product_id' => $creditProduct->getKey(),
|
||||
'billing_product_purpose' => $creditProduct->purpose->value,
|
||||
'billing_product_id' => $billingProduct->getKey(),
|
||||
'billing_product_purpose' => $billingProduct->purpose->value,
|
||||
];
|
||||
$sessionOptions = [
|
||||
'success_url' => $this->redirectUrl('success'),
|
||||
'cancel_url' => $this->redirectUrl('cancel'),
|
||||
'metadata' => $metadata,
|
||||
'client_reference_id' => $creditProduct->getKey(),
|
||||
'client_reference_id' => $billingProduct->getKey(),
|
||||
];
|
||||
|
||||
if ($creditProduct->purpose === BillingProductPurpose::SUBSCRIPTION) {
|
||||
if ($billingProduct->purpose === BillingProductPurpose::SUBSCRIPTION) {
|
||||
if ($user->subscribed('default')) {
|
||||
return response()->json([
|
||||
'message' => __('api.billing.active_subscription_exists'),
|
||||
@@ -58,7 +58,7 @@ class BillingController extends Controller
|
||||
}
|
||||
|
||||
$checkout = $user
|
||||
->newSubscription('default', $creditProduct->stripe_price_id)
|
||||
->newSubscription('default', $billingProduct->stripe_price_id)
|
||||
->withMetadata($metadata)
|
||||
->checkout($sessionOptions);
|
||||
} else {
|
||||
@@ -76,7 +76,7 @@ class BillingController extends Controller
|
||||
];
|
||||
|
||||
$checkout = $user->checkout([
|
||||
$creditProduct->stripe_price_id => 1,
|
||||
$billingProduct->stripe_price_id => 1,
|
||||
], $sessionOptions);
|
||||
}
|
||||
|
||||
@@ -84,7 +84,7 @@ class BillingController extends Controller
|
||||
|
||||
$transactions->recordCheckoutSession(
|
||||
user: $user,
|
||||
product: $creditProduct,
|
||||
product: $billingProduct,
|
||||
stripeCheckoutSessionId: $session->id,
|
||||
stripeCustomerId: $session->customer,
|
||||
amount: $session->amount_total,
|
||||
|
||||
+1
-1
@@ -5,7 +5,7 @@ namespace App\Http\Resources;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Resources\Json\JsonResource;
|
||||
|
||||
class CreditProductResource extends JsonResource
|
||||
class BillingProductResource extends JsonResource
|
||||
{
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
+8
-13
@@ -4,13 +4,13 @@ namespace App\Listeners;
|
||||
|
||||
use App\Enums\BillingProductPurpose;
|
||||
use App\Enums\PaymentTransactionStatus;
|
||||
use App\Models\CreditProduct;
|
||||
use App\Models\BillingProduct;
|
||||
use App\Models\User;
|
||||
use App\Services\PaymentInvoiceEmailer;
|
||||
use App\Services\PaymentTransactionRecorder;
|
||||
use Laravel\Cashier\Cashier;
|
||||
|
||||
class GrantCreditsFromStripeWebhook
|
||||
class HandleStripeWebhook
|
||||
{
|
||||
public function __construct(
|
||||
private PaymentInvoiceEmailer $invoiceEmails,
|
||||
@@ -92,7 +92,6 @@ class GrantCreditsFromStripeWebhook
|
||||
stripePaymentIntentId: $this->stringValue($session['payment_intent'] ?? null),
|
||||
amount: $this->intValue($session['amount_total'] ?? null),
|
||||
currency: $this->stringValue($session['currency'] ?? null),
|
||||
creditsExpected: 0,
|
||||
invoiceUrl: $invoice ? $this->stringValue($invoice['hosted_invoice_url'] ?? null) : null,
|
||||
invoicePdfUrl: $invoice ? $this->stringValue($invoice['invoice_pdf'] ?? null) : null,
|
||||
payload: $session,
|
||||
@@ -124,7 +123,6 @@ class GrantCreditsFromStripeWebhook
|
||||
stripePaymentIntentId: $this->stringValue($session['payment_intent'] ?? null),
|
||||
amount: $this->intValue($session['amount_total'] ?? null),
|
||||
currency: $this->stringValue($session['currency'] ?? null),
|
||||
creditsExpected: 0,
|
||||
errorMessage: 'Checkout session expired before payment.',
|
||||
payload: $session,
|
||||
);
|
||||
@@ -147,7 +145,7 @@ class GrantCreditsFromStripeWebhook
|
||||
foreach (($invoice['lines']['data'] ?? []) as $line) {
|
||||
$priceId = $this->linePriceId($line);
|
||||
$product = $priceId
|
||||
? CreditProduct::query()
|
||||
? BillingProduct::query()
|
||||
->where('purpose', BillingProductPurpose::SUBSCRIPTION)
|
||||
->where('stripe_price_id', $priceId)
|
||||
->first()
|
||||
@@ -171,7 +169,6 @@ class GrantCreditsFromStripeWebhook
|
||||
stripeSubscriptionId: $this->invoiceSubscriptionId($invoice),
|
||||
amount: $this->intValue($line['amount'] ?? $invoice['amount_paid'] ?? null),
|
||||
currency: $this->stringValue($invoice['currency'] ?? null),
|
||||
creditsExpected: 0,
|
||||
invoiceUrl: $this->stringValue($invoice['hosted_invoice_url'] ?? null),
|
||||
invoicePdfUrl: $this->stringValue($invoice['invoice_pdf'] ?? null),
|
||||
payload: $invoice,
|
||||
@@ -194,7 +191,6 @@ class GrantCreditsFromStripeWebhook
|
||||
stripeSubscriptionId: $this->invoiceSubscriptionId($invoice),
|
||||
amount: $this->intValue($invoice['amount_paid'] ?? null),
|
||||
currency: $this->stringValue($invoice['currency'] ?? null),
|
||||
creditsExpected: 0,
|
||||
errorMessage: 'Invoice does not contain the active subscription product.',
|
||||
payload: $invoice,
|
||||
);
|
||||
@@ -218,7 +214,7 @@ class GrantCreditsFromStripeWebhook
|
||||
foreach ($lines ?: [null] as $line) {
|
||||
$priceId = is_array($line) ? $this->linePriceId($line) : null;
|
||||
$product = $priceId
|
||||
? CreditProduct::query()->where('stripe_price_id', $priceId)->first()
|
||||
? BillingProduct::query()->where('stripe_price_id', $priceId)->first()
|
||||
: null;
|
||||
|
||||
$this->transactions->recordInvoice(
|
||||
@@ -238,7 +234,6 @@ class GrantCreditsFromStripeWebhook
|
||||
: ($invoice['amount_due'] ?? null)
|
||||
),
|
||||
currency: $this->stringValue($invoice['currency'] ?? null),
|
||||
creditsExpected: 0,
|
||||
errorMessage: 'Invoice payment failed.',
|
||||
payload: $invoice,
|
||||
);
|
||||
@@ -254,10 +249,10 @@ class GrantCreditsFromStripeWebhook
|
||||
return User::query()->where('stripe_id', $customer)->first();
|
||||
}
|
||||
|
||||
private function productFromMetadata(mixed $metadata): ?CreditProduct
|
||||
private function productFromMetadata(mixed $metadata): ?BillingProduct
|
||||
{
|
||||
if (! is_array($metadata)
|
||||
|| ! is_string($metadata['credit_product_id'] ?? null)
|
||||
|| ! is_string($metadata['billing_product_id'] ?? null)
|
||||
|| ! is_string($metadata['billing_product_purpose'] ?? null)) {
|
||||
return null;
|
||||
}
|
||||
@@ -268,8 +263,8 @@ class GrantCreditsFromStripeWebhook
|
||||
return null;
|
||||
}
|
||||
|
||||
return CreditProduct::query()
|
||||
->whereKey($metadata['credit_product_id'])
|
||||
return BillingProduct::query()
|
||||
->whereKey($metadata['billing_product_id'])
|
||||
->where('purpose', $purpose)
|
||||
->first();
|
||||
}
|
||||
@@ -32,11 +32,10 @@ class PaymentInvoiceMail extends Mailable
|
||||
view: 'emails.payment-invoice',
|
||||
with: [
|
||||
'amount' => $this->formattedAmount(),
|
||||
'credits' => $this->transaction->credits_granted ?: $this->transaction->credits_expected,
|
||||
'invoicePdfUrl' => $this->transaction->invoice_pdf_url,
|
||||
'invoiceUrl' => $this->transaction->invoice_url,
|
||||
'locale' => $this->mailLocale(),
|
||||
'productName' => $this->transaction->creditProduct?->name,
|
||||
'productName' => $this->transaction->billingProduct?->name,
|
||||
'userName' => $this->user->name,
|
||||
],
|
||||
);
|
||||
|
||||
@@ -3,24 +3,20 @@
|
||||
namespace App\Models;
|
||||
|
||||
use App\Enums\BillingProductPurpose;
|
||||
use App\Enums\CreditProductType;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Concerns\HasUlids;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
|
||||
class CreditProduct extends Model
|
||||
class BillingProduct extends Model
|
||||
{
|
||||
/** @use HasFactory<\Database\Factories\CreditProductFactory> */
|
||||
use HasFactory, HasUlids;
|
||||
|
||||
protected $fillable = [
|
||||
'name',
|
||||
'description',
|
||||
'type',
|
||||
'purpose',
|
||||
'credits',
|
||||
'amount',
|
||||
'currency',
|
||||
'stripe_product_id',
|
||||
@@ -29,11 +25,6 @@ class CreditProduct extends Model
|
||||
'sort_order',
|
||||
];
|
||||
|
||||
public function ledgerEntries(): HasMany
|
||||
{
|
||||
return $this->hasMany(CreditLedgerEntry::class);
|
||||
}
|
||||
|
||||
public function paymentTransactions(): HasMany
|
||||
{
|
||||
return $this->hasMany(PaymentTransaction::class);
|
||||
@@ -52,9 +43,7 @@ class CreditProduct extends Model
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'type' => CreditProductType::class,
|
||||
'purpose' => BillingProductPurpose::class,
|
||||
'credits' => 'integer',
|
||||
'amount' => 'integer',
|
||||
'is_active' => 'boolean',
|
||||
'sort_order' => 'integer',
|
||||
@@ -1,49 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Enums\CreditLedgerEntryType;
|
||||
use Illuminate\Database\Eloquent\Concerns\HasUlids;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class CreditLedgerEntry extends Model
|
||||
{
|
||||
/** @use HasFactory<\Database\Factories\CreditLedgerEntryFactory> */
|
||||
use HasFactory, HasUlids;
|
||||
|
||||
protected $fillable = [
|
||||
'user_id',
|
||||
'credit_product_id',
|
||||
'type',
|
||||
'credits',
|
||||
'balance_after',
|
||||
'source',
|
||||
'stripe_checkout_session_id',
|
||||
'stripe_invoice_id',
|
||||
'stripe_payment_intent_id',
|
||||
'stripe_subscription_id',
|
||||
'metadata',
|
||||
];
|
||||
|
||||
public function user(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class);
|
||||
}
|
||||
|
||||
public function creditProduct(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(CreditProduct::class);
|
||||
}
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'type' => CreditLedgerEntryType::class,
|
||||
'credits' => 'integer',
|
||||
'balance_after' => 'integer',
|
||||
'metadata' => 'array',
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -16,8 +16,7 @@ class PaymentTransaction extends Model
|
||||
|
||||
protected $fillable = [
|
||||
'user_id',
|
||||
'credit_product_id',
|
||||
'credit_ledger_entry_id',
|
||||
'billing_product_id',
|
||||
'type',
|
||||
'status',
|
||||
'stripe_event_id',
|
||||
@@ -30,8 +29,6 @@ class PaymentTransaction extends Model
|
||||
'stripe_price_id',
|
||||
'amount',
|
||||
'currency',
|
||||
'credits_expected',
|
||||
'credits_granted',
|
||||
'invoice_url',
|
||||
'invoice_pdf_url',
|
||||
'invoice_email_sent_at',
|
||||
@@ -45,14 +42,9 @@ class PaymentTransaction extends Model
|
||||
return $this->belongsTo(User::class);
|
||||
}
|
||||
|
||||
public function creditProduct(): BelongsTo
|
||||
public function billingProduct(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(CreditProduct::class);
|
||||
}
|
||||
|
||||
public function creditLedgerEntry(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(CreditLedgerEntry::class);
|
||||
return $this->belongsTo(BillingProduct::class);
|
||||
}
|
||||
|
||||
protected function casts(): array
|
||||
@@ -61,8 +53,6 @@ class PaymentTransaction extends Model
|
||||
'type' => PaymentTransactionType::class,
|
||||
'status' => PaymentTransactionStatus::class,
|
||||
'amount' => 'integer',
|
||||
'credits_expected' => 'integer',
|
||||
'credits_granted' => 'integer',
|
||||
'invoice_email_sent_at' => 'datetime',
|
||||
'payload' => 'array',
|
||||
'processed_at' => 'datetime',
|
||||
|
||||
@@ -61,7 +61,6 @@ class User extends Authenticatable implements FilamentUser, HasAvatar, HasLocale
|
||||
'suspended_at',
|
||||
'suspended_by',
|
||||
'suspended_reason',
|
||||
'ai_credits_balance',
|
||||
];
|
||||
|
||||
/**
|
||||
@@ -98,7 +97,6 @@ class User extends Authenticatable implements FilamentUser, HasAvatar, HasLocale
|
||||
'date_of_birth' => 'immutable_date',
|
||||
'suspended_at' => 'datetime',
|
||||
'trial_ends_at' => 'datetime',
|
||||
'ai_credits_balance' => 'integer',
|
||||
];
|
||||
}
|
||||
|
||||
@@ -122,11 +120,6 @@ class User extends Authenticatable implements FilamentUser, HasAvatar, HasLocale
|
||||
return $this->hasMany(AiUsage::class);
|
||||
}
|
||||
|
||||
public function creditLedgerEntries(): HasMany
|
||||
{
|
||||
return $this->hasMany(CreditLedgerEntry::class);
|
||||
}
|
||||
|
||||
public function paymentTransactions(): HasMany
|
||||
{
|
||||
return $this->hasMany(PaymentTransaction::class);
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
namespace App\Providers;
|
||||
|
||||
use App\Broadcasting\ExpoPushChannel;
|
||||
use App\Listeners\GrantCreditsFromStripeWebhook;
|
||||
use App\Listeners\HandleStripeWebhook;
|
||||
use App\Mail\ResetPasswordMail;
|
||||
use App\Mail\VerifyAccount;
|
||||
use App\Models\User;
|
||||
@@ -60,7 +60,7 @@ class AppServiceProvider extends ServiceProvider
|
||||
|
||||
Notification::extend('expo', fn ($app) => $app->make(ExpoPushChannel::class));
|
||||
|
||||
Event::listen(\Laravel\Cashier\Events\WebhookReceived::class, GrantCreditsFromStripeWebhook::class);
|
||||
Event::listen(\Laravel\Cashier\Events\WebhookReceived::class, HandleStripeWebhook::class);
|
||||
|
||||
VerifyEmail::createUrlUsing(function (User $notifiable): string {
|
||||
$relativeUrl = URL::temporarySignedRoute(
|
||||
|
||||
@@ -1,142 +0,0 @@
|
||||
<?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,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -25,7 +25,7 @@ class PaymentInvoiceEmailer
|
||||
|
||||
try {
|
||||
Mail::to($transaction->user->email)->send(
|
||||
new PaymentInvoiceMail($transaction->user, $transaction->loadMissing('creditProduct'))
|
||||
new PaymentInvoiceMail($transaction->user, $transaction->loadMissing('billingProduct'))
|
||||
);
|
||||
} catch (Throwable $exception) {
|
||||
$transaction->forceFill([
|
||||
|
||||
@@ -4,8 +4,7 @@ namespace App\Services;
|
||||
|
||||
use App\Enums\PaymentTransactionStatus;
|
||||
use App\Enums\PaymentTransactionType;
|
||||
use App\Models\CreditLedgerEntry;
|
||||
use App\Models\CreditProduct;
|
||||
use App\Models\BillingProduct;
|
||||
use App\Models\PaymentTransaction;
|
||||
use App\Models\User;
|
||||
|
||||
@@ -16,7 +15,7 @@ class PaymentTransactionRecorder
|
||||
*/
|
||||
public function recordCheckoutSession(
|
||||
?User $user,
|
||||
?CreditProduct $product,
|
||||
?BillingProduct $product,
|
||||
string $stripeCheckoutSessionId,
|
||||
PaymentTransactionStatus $status = PaymentTransactionStatus::PENDING,
|
||||
?string $stripeEventId = null,
|
||||
@@ -25,9 +24,6 @@ class PaymentTransactionRecorder
|
||||
?string $stripePaymentIntentId = null,
|
||||
?int $amount = null,
|
||||
?string $currency = null,
|
||||
?int $creditsExpected = null,
|
||||
?int $creditsGranted = null,
|
||||
?CreditLedgerEntry $ledgerEntry = null,
|
||||
?string $invoiceUrl = null,
|
||||
?string $invoicePdfUrl = null,
|
||||
?string $errorMessage = null,
|
||||
@@ -37,8 +33,7 @@ class PaymentTransactionRecorder
|
||||
'stripe_checkout_session_id' => $stripeCheckoutSessionId,
|
||||
], [
|
||||
'user_id' => $user?->getKey(),
|
||||
'credit_product_id' => $product?->getKey(),
|
||||
'credit_ledger_entry_id' => $ledgerEntry?->getKey(),
|
||||
'billing_product_id' => $product?->getKey(),
|
||||
'type' => PaymentTransactionType::CHECKOUT,
|
||||
'status' => $status,
|
||||
'stripe_event_id' => $stripeEventId,
|
||||
@@ -47,8 +42,6 @@ class PaymentTransactionRecorder
|
||||
'stripe_payment_intent_id' => $stripePaymentIntentId,
|
||||
'amount' => $amount,
|
||||
'currency' => $currency,
|
||||
'credits_expected' => $creditsExpected ?? $product?->credits,
|
||||
'credits_granted' => $creditsGranted ?? 0,
|
||||
'invoice_url' => $invoiceUrl,
|
||||
'invoice_pdf_url' => $invoicePdfUrl,
|
||||
'error_message' => $errorMessage,
|
||||
@@ -62,7 +55,7 @@ class PaymentTransactionRecorder
|
||||
*/
|
||||
public function recordInvoice(
|
||||
?User $user,
|
||||
?CreditProduct $product,
|
||||
?BillingProduct $product,
|
||||
string $stripeInvoiceId,
|
||||
?string $stripePriceId,
|
||||
PaymentTransactionStatus $status,
|
||||
@@ -73,9 +66,6 @@ class PaymentTransactionRecorder
|
||||
?string $stripeSubscriptionId = null,
|
||||
?int $amount = null,
|
||||
?string $currency = null,
|
||||
?int $creditsExpected = null,
|
||||
?int $creditsGranted = null,
|
||||
?CreditLedgerEntry $ledgerEntry = null,
|
||||
?string $invoiceUrl = null,
|
||||
?string $invoicePdfUrl = null,
|
||||
?string $errorMessage = null,
|
||||
@@ -86,8 +76,7 @@ class PaymentTransactionRecorder
|
||||
'stripe_price_id' => $stripePriceId,
|
||||
], [
|
||||
'user_id' => $user?->getKey(),
|
||||
'credit_product_id' => $product?->getKey(),
|
||||
'credit_ledger_entry_id' => $ledgerEntry?->getKey(),
|
||||
'billing_product_id' => $product?->getKey(),
|
||||
'type' => PaymentTransactionType::SUBSCRIPTION_INVOICE,
|
||||
'status' => $status,
|
||||
'stripe_event_id' => $stripeEventId,
|
||||
@@ -97,8 +86,6 @@ class PaymentTransactionRecorder
|
||||
'stripe_subscription_id' => $stripeSubscriptionId,
|
||||
'amount' => $amount,
|
||||
'currency' => $currency,
|
||||
'credits_expected' => $creditsExpected ?? $product?->credits,
|
||||
'credits_granted' => $creditsGranted ?? 0,
|
||||
'invoice_url' => $invoiceUrl,
|
||||
'invoice_pdf_url' => $invoicePdfUrl,
|
||||
'error_message' => $errorMessage,
|
||||
|
||||
+6
-6
@@ -3,12 +3,12 @@
|
||||
namespace App\Services;
|
||||
|
||||
use App\Enums\BillingProductPurpose;
|
||||
use App\Models\CreditProduct;
|
||||
use App\Models\BillingProduct;
|
||||
use Laravel\Cashier\Cashier;
|
||||
|
||||
class StripeCreditProductSyncer
|
||||
class StripeBillingProductSyncer
|
||||
{
|
||||
public function sync(CreditProduct $product): CreditProduct
|
||||
public function sync(BillingProduct $product): BillingProduct
|
||||
{
|
||||
throw_if(
|
||||
$product->purpose === null,
|
||||
@@ -24,7 +24,7 @@ class StripeCreditProductSyncer
|
||||
'name' => $product->name,
|
||||
'description' => $product->description,
|
||||
'metadata' => [
|
||||
'credit_product_id' => $product->getKey(),
|
||||
'billing_product_id' => $product->getKey(),
|
||||
'billing_product_purpose' => $product->purpose?->value,
|
||||
],
|
||||
]);
|
||||
@@ -36,7 +36,7 @@ class StripeCreditProductSyncer
|
||||
'description' => $product->description,
|
||||
'active' => (bool) $product->is_active,
|
||||
'metadata' => [
|
||||
'credit_product_id' => $product->getKey(),
|
||||
'billing_product_id' => $product->getKey(),
|
||||
'billing_product_purpose' => $product->purpose?->value,
|
||||
],
|
||||
]);
|
||||
@@ -58,7 +58,7 @@ class StripeCreditProductSyncer
|
||||
'unit_amount' => $product->amount,
|
||||
'currency' => strtolower($product->currency),
|
||||
'metadata' => [
|
||||
'credit_product_id' => $product->getKey(),
|
||||
'billing_product_id' => $product->getKey(),
|
||||
'billing_product_purpose' => $product->purpose?->value,
|
||||
],
|
||||
];
|
||||
Reference in New Issue
Block a user