Merge pull request 'DX audit and fix (PHP)' (#22) from agent/dx-audit-and-fix--laravel-php-package into dev

Reviewed-on: #22
This commit is contained in:
Snider 2026-03-24 11:36:42 +00:00
commit b51084b8db
95 changed files with 458 additions and 207 deletions

View file

@ -8,11 +8,32 @@ use Core\Events\AdminPanelBooting;
use Core\Events\ApiRoutesRegistering;
use Core\Events\ConsoleBooting;
use Core\Events\WebRoutesRegistering;
use Core\Mod\Commerce\Events\OrderPaid;
use Core\Mod\Commerce\Events\SubscriptionCreated;
use Core\Mod\Commerce\Events\SubscriptionRenewed;
use Core\Mod\Commerce\Listeners\ProvisionSocialHostSubscription;
use Core\Mod\Commerce\Listeners\RewardAgentReferralOnSubscription;
use Core\Mod\Commerce\Services\CheckoutRateLimiter;
use Core\Mod\Commerce\Services\CommerceService;
use Core\Mod\Commerce\Services\ContentOverrideService;
use Core\Mod\Commerce\Services\CouponService;
use Core\Mod\Commerce\Services\CreditNoteService;
use Core\Mod\Commerce\Services\CurrencyService;
use Core\Mod\Commerce\Services\DunningService;
use Core\Mod\Commerce\Services\FraudService;
use Core\Mod\Commerce\Services\InvoiceService;
use Core\Mod\Commerce\Services\PaymentGateway\BTCPayGateway;
use Core\Mod\Commerce\Services\PaymentGateway\PaymentGatewayContract;
use Core\Mod\Commerce\Services\PaymentGateway\StripeGateway;
use Core\Mod\Commerce\Services\PaymentMethodService;
use Core\Mod\Commerce\Services\PermissionMatrixService;
use Core\Mod\Commerce\Services\ReferralService;
use Core\Mod\Commerce\Services\SkuBuilderService;
use Core\Mod\Commerce\Services\SkuParserService;
use Core\Mod\Commerce\Services\SubscriptionService;
use Core\Mod\Commerce\Services\TaxService;
use Core\Mod\Commerce\Services\UsageBillingService;
use Core\Mod\Commerce\Services\WebhookRateLimiter;
use Illuminate\Support\Facades\Event;
use Illuminate\Support\Facades\Route;
use Illuminate\Support\ServiceProvider;
@ -46,9 +67,9 @@ class Boot extends ServiceProvider
// Laravel event listeners (not lifecycle events)
Event::subscribe(ProvisionSocialHostSubscription::class);
Event::listen(\Core\Mod\Commerce\Events\SubscriptionCreated::class, RewardAgentReferralOnSubscription::class);
Event::listen(\Core\Mod\Commerce\Events\SubscriptionRenewed::class, Listeners\ResetUsageOnRenewal::class);
Event::listen(\Core\Mod\Commerce\Events\OrderPaid::class, Listeners\CreateReferralCommission::class);
Event::listen(SubscriptionCreated::class, RewardAgentReferralOnSubscription::class);
Event::listen(SubscriptionRenewed::class, Listeners\ResetUsageOnRenewal::class);
Event::listen(OrderPaid::class, Listeners\CreateReferralCommission::class);
}
public function register(): void
@ -59,24 +80,24 @@ class Boot extends ServiceProvider
);
// Core Services
$this->app->singleton(\Core\Mod\Commerce\Services\CommerceService::class);
$this->app->singleton(\Core\Mod\Commerce\Services\SubscriptionService::class);
$this->app->singleton(\Core\Mod\Commerce\Services\InvoiceService::class);
$this->app->singleton(\Core\Mod\Commerce\Services\PermissionMatrixService::class);
$this->app->singleton(\Core\Mod\Commerce\Services\CouponService::class);
$this->app->singleton(\Core\Mod\Commerce\Services\TaxService::class);
$this->app->singleton(\Core\Mod\Commerce\Services\CurrencyService::class);
$this->app->singleton(\Core\Mod\Commerce\Services\ContentOverrideService::class);
$this->app->singleton(\Core\Mod\Commerce\Services\DunningService::class);
$this->app->singleton(\Core\Mod\Commerce\Services\SkuParserService::class);
$this->app->singleton(\Core\Mod\Commerce\Services\SkuBuilderService::class);
$this->app->singleton(\Core\Mod\Commerce\Services\CreditNoteService::class);
$this->app->singleton(\Core\Mod\Commerce\Services\PaymentMethodService::class);
$this->app->singleton(\Core\Mod\Commerce\Services\UsageBillingService::class);
$this->app->singleton(\Core\Mod\Commerce\Services\ReferralService::class);
$this->app->singleton(\Core\Mod\Commerce\Services\FraudService::class);
$this->app->singleton(\Core\Mod\Commerce\Services\CheckoutRateLimiter::class);
$this->app->singleton(\Core\Mod\Commerce\Services\WebhookRateLimiter::class);
$this->app->singleton(CommerceService::class);
$this->app->singleton(SubscriptionService::class);
$this->app->singleton(InvoiceService::class);
$this->app->singleton(PermissionMatrixService::class);
$this->app->singleton(CouponService::class);
$this->app->singleton(TaxService::class);
$this->app->singleton(CurrencyService::class);
$this->app->singleton(ContentOverrideService::class);
$this->app->singleton(DunningService::class);
$this->app->singleton(SkuParserService::class);
$this->app->singleton(SkuBuilderService::class);
$this->app->singleton(CreditNoteService::class);
$this->app->singleton(PaymentMethodService::class);
$this->app->singleton(UsageBillingService::class);
$this->app->singleton(ReferralService::class);
$this->app->singleton(FraudService::class);
$this->app->singleton(CheckoutRateLimiter::class);
$this->app->singleton(WebhookRateLimiter::class);
// Payment Gateways
$this->app->singleton('commerce.gateway.btcpay', function ($app) {

View file

@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
namespace Core\Mod\Commerce\Console;
use Core\Mod\Commerce\Models\Order;

View file

@ -4,6 +4,7 @@ declare(strict_types=1);
namespace Core\Mod\Commerce\Console;
use Carbon\Carbon;
use Core\Mod\Commerce\Models\Subscription;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Log;
@ -164,7 +165,7 @@ class PlantSubscriberTrees extends Command
protected function hasPlantedThisMonth(int $workspaceId, string $month): bool
{
// Parse the month string (YYYY-MM format)
$date = \Carbon\Carbon::createFromFormat('Y-m', $month);
$date = Carbon::createFromFormat('Y-m', $month);
$startOfMonth = $date->copy()->startOfMonth();
$endOfMonth = $date->copy()->endOfMonth();

View file

@ -1,7 +1,10 @@
<?php
declare(strict_types=1);
namespace Core\Mod\Commerce\Console;
use Core\Mod\Commerce\Models\Subscription;
use Core\Mod\Commerce\Services\DunningService;
use Core\Mod\Commerce\Services\SubscriptionService;
use Illuminate\Console\Command;
@ -269,7 +272,7 @@ class ProcessDunning extends Command
$this->info('Stage 5: Expired Subscriptions');
if ($dryRun) {
$count = \Core\Mod\Commerce\Models\Subscription::query()
$count = Subscription::query()
->active()
->whereNotNull('cancelled_at')
->where('current_period_end', '<=', now())

View file

@ -4,6 +4,7 @@ declare(strict_types=1);
namespace Core\Mod\Commerce\Console;
use Core\Mod\Commerce\Models\ExchangeRate;
use Core\Mod\Commerce\Services\CurrencyService;
use Illuminate\Console\Command;
@ -30,7 +31,7 @@ class RefreshExchangeRates extends Command
$this->line("Provider: {$provider}");
// Check if rates need refresh
if (! $this->option('force') && ! \Core\Mod\Commerce\Models\ExchangeRate::needsRefresh()) {
if (! $this->option('force') && ! ExchangeRate::needsRefresh()) {
$this->info('Rates are still fresh. Use --force to refresh anyway.');
return self::SUCCESS;

View file

@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
namespace Core\Mod\Commerce\Console;
use Core\Mod\Commerce\Models\Subscription;

View file

@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
namespace Core\Mod\Commerce\Console;
use Core\Mod\Commerce\Models\Subscription;

View file

@ -12,7 +12,9 @@ use Core\Mod\Commerce\Services\CommerceService;
use Core\Mod\Commerce\Services\InvoiceService;
use Core\Mod\Commerce\Services\SubscriptionService;
use Core\Tenant\Models\Package;
use Core\Tenant\Models\User;
use Core\Tenant\Models\Workspace;
use Core\Tenant\Services\EntitlementService;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
@ -38,7 +40,7 @@ class CommerceController extends Controller
{
$user = Auth::user();
if (! $user instanceof \Core\Tenant\Models\User) {
if (! $user instanceof User) {
return null;
}
@ -200,7 +202,7 @@ class CommerceController extends Controller
return response()->json(['error' => 'No workspace found'], 404);
}
$entitlements = app(\Core\Tenant\Services\EntitlementService::class);
$entitlements = app(EntitlementService::class);
$summary = $entitlements->getUsageSummary($workspace);
return response()->json([

View file

@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
namespace Core\Mod\Commerce\Data;
use Core\Mod\Commerce\Models\Coupon;

View file

@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
namespace Core\Mod\Commerce\Events;
use Core\Mod\Commerce\Models\Subscription;

View file

@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
namespace Core\Mod\Commerce\Events;
use Core\Mod\Commerce\Models\Subscription;

View file

@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
namespace Core\Mod\Commerce\Events;
use Core\Mod\Commerce\Models\Subscription;

View file

@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
namespace Core\Mod\Commerce\Events;
use Core\Mod\Commerce\Models\Subscription;

View file

@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
namespace Core\Mod\Commerce\Listeners;
use Core\Mod\Commerce\Events\SubscriptionCancelled;

View file

@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
namespace Core\Mod\Commerce\Listeners;
use Core\Mod\Commerce\Events\SubscriptionRenewed;

View file

@ -1,7 +1,10 @@
<?php
declare(strict_types=1);
namespace Core\Mod\Commerce\Mcp\Tools;
use Carbon\Carbon;
use Core\Mod\Commerce\Models\Coupon;
use Illuminate\Contracts\JsonSchema\JsonSchema;
use Laravel\Mcp\Request;
@ -59,7 +62,7 @@ class CreateCoupon extends Tool
'duration' => $duration,
'max_uses' => $maxUses,
'max_uses_per_workspace' => 1,
'valid_until' => $validUntil ? \Carbon\Carbon::parse($validUntil) : null,
'valid_until' => $validUntil ? Carbon::parse($validUntil) : null,
'is_active' => true,
'applies_to' => 'all',
]);

View file

@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
namespace Core\Mod\Commerce\Mcp\Tools;
use Core\Mod\Commerce\Models\Subscription;

View file

@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
namespace Core\Mod\Commerce\Mcp\Tools;
use Core\Mod\Commerce\Models\Invoice;

View file

@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
namespace Core\Mod\Commerce\Mcp\Tools;
use Core\Mod\Commerce\Models\Subscription;

View file

@ -1,8 +1,12 @@
<?php
declare(strict_types=1);
namespace Core\Mod\Commerce\Models;
use Carbon\Carbon;
use Core\Mod\Commerce\Contracts\Orderable;
use Core\Mod\Commerce\Database\Factories\CouponFactory;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
@ -27,8 +31,8 @@ use Spatie\Activitylog\Traits\LogsActivity;
* @property int $used_count
* @property string $duration
* @property int|null $duration_months
* @property \Carbon\Carbon|null $valid_from
* @property \Carbon\Carbon|null $valid_until
* @property Carbon|null $valid_from
* @property Carbon|null $valid_until
* @property bool $is_active
*/
class Coupon extends Model
@ -36,9 +40,9 @@ class Coupon extends Model
use HasFactory;
use LogsActivity;
protected static function newFactory(): \Core\Mod\Commerce\Database\Factories\CouponFactory
protected static function newFactory(): CouponFactory
{
return \Core\Mod\Commerce\Database\Factories\CouponFactory::new();
return CouponFactory::new();
}
protected $fillable = [

View file

@ -1,7 +1,10 @@
<?php
declare(strict_types=1);
namespace Core\Mod\Commerce\Models;
use Carbon\Carbon;
use Core\Tenant\Models\Workspace;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
@ -14,7 +17,7 @@ use Illuminate\Database\Eloquent\Relations\BelongsTo;
* @property int $workspace_id
* @property int $order_id
* @property float $discount_amount
* @property \Carbon\Carbon $created_at
* @property Carbon $created_at
*/
class CouponUsage extends Model
{

View file

@ -1,7 +1,10 @@
<?php
declare(strict_types=1);
namespace Core\Mod\Commerce\Models;
use Carbon\Carbon;
use Core\Tenant\Models\User;
use Core\Tenant\Models\Workspace;
use Illuminate\Database\Eloquent\Factories\HasFactory;
@ -31,9 +34,9 @@ use Spatie\Activitylog\Traits\LogsActivity;
* @property string $status
* @property float $amount_used
* @property int|null $applied_to_order_id
* @property \Carbon\Carbon|null $issued_at
* @property \Carbon\Carbon|null $applied_at
* @property \Carbon\Carbon|null $voided_at
* @property Carbon|null $issued_at
* @property Carbon|null $applied_at
* @property Carbon|null $voided_at
* @property int|null $issued_by
* @property int|null $voided_by
* @property array|null $metadata

View file

@ -4,6 +4,7 @@ declare(strict_types=1);
namespace Core\Mod\Commerce\Models;
use Carbon\Carbon;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\Cache;
@ -15,7 +16,7 @@ use Illuminate\Support\Facades\Cache;
* @property string $target_currency
* @property float $rate
* @property string $source
* @property \Carbon\Carbon $fetched_at
* @property Carbon $fetched_at
*/
class ExchangeRate extends Model
{

View file

@ -4,6 +4,7 @@ declare(strict_types=1);
namespace Core\Mod\Commerce\Models;
use Carbon\Carbon;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
@ -20,8 +21,8 @@ use Illuminate\Database\Eloquent\Relations\HasMany;
* @property int|null $low_stock_threshold
* @property string|null $bin_location
* @property string|null $zone
* @property \Carbon\Carbon|null $last_counted_at
* @property \Carbon\Carbon|null $last_restocked_at
* @property Carbon|null $last_counted_at
* @property Carbon|null $last_restocked_at
* @property int|null $unit_cost
* @property array|null $metadata
*/

View file

@ -4,6 +4,7 @@ declare(strict_types=1);
namespace Core\Mod\Commerce\Models;
use Carbon\Carbon;
use Core\Tenant\Models\User;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
@ -24,7 +25,7 @@ use Illuminate\Database\Eloquent\Relations\BelongsTo;
* @property string|null $notes
* @property int|null $user_id
* @property int|null $unit_cost
* @property \Carbon\Carbon $created_at
* @property Carbon $created_at
*/
class InventoryMovement extends Model
{

View file

@ -1,7 +1,11 @@
<?php
declare(strict_types=1);
namespace Core\Mod\Commerce\Models;
use Carbon\Carbon;
use Core\Mod\Commerce\Database\Factories\InvoiceFactory;
use Core\Tenant\Models\Workspace;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
@ -23,9 +27,9 @@ use Illuminate\Database\Eloquent\Relations\HasMany;
* @property float $total
* @property float $amount_paid
* @property float $amount_due
* @property \Carbon\Carbon $issue_date
* @property \Carbon\Carbon $due_date
* @property \Carbon\Carbon|null $paid_at
* @property Carbon $issue_date
* @property Carbon $due_date
* @property Carbon|null $paid_at
* @property string|null $billing_name
* @property array|null $billing_address
* @property string|null $tax_id
@ -35,9 +39,9 @@ class Invoice extends Model
{
use HasFactory;
protected static function newFactory(): \Core\Mod\Commerce\Database\Factories\InvoiceFactory
protected static function newFactory(): InvoiceFactory
{
return \Core\Mod\Commerce\Database\Factories\InvoiceFactory::new();
return InvoiceFactory::new();
}
protected $fillable = [
@ -93,7 +97,7 @@ class Invoice extends Model
/**
* Get the issued_at attribute (alias for issue_date).
*/
public function getIssuedAtAttribute(): ?\Carbon\Carbon
public function getIssuedAtAttribute(): ?Carbon
{
return $this->issue_date;
}

View file

@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
namespace Core\Mod\Commerce\Models;
use Illuminate\Database\Eloquent\Model;

View file

@ -1,8 +1,13 @@
<?php
declare(strict_types=1);
namespace Core\Mod\Commerce\Models;
use Carbon\Carbon;
use Core\Mod\Commerce\Contracts\Orderable;
use Core\Mod\Commerce\Database\Factories\OrderFactory;
use Core\Mod\Commerce\Services\CurrencyService;
use Core\Tenant\Models\User;
use Core\Tenant\Models\Workspace;
use Illuminate\Database\Eloquent\Factories\HasFactory;
@ -38,7 +43,7 @@ use Spatie\Activitylog\Traits\LogsActivity;
* @property int|null $coupon_id
* @property array|null $billing_address
* @property array|null $metadata
* @property \Carbon\Carbon|null $paid_at
* @property Carbon|null $paid_at
* @property-read Orderable|null $orderable
*/
class Order extends Model
@ -46,9 +51,9 @@ class Order extends Model
use HasFactory;
use LogsActivity;
protected static function newFactory(): \Core\Mod\Commerce\Database\Factories\OrderFactory
protected static function newFactory(): OrderFactory
{
return \Core\Mod\Commerce\Database\Factories\OrderFactory::new();
return OrderFactory::new();
}
protected $fillable = [
@ -291,7 +296,7 @@ class Order extends Model
*/
public function getFormattedTotalAttribute(): string
{
$currencyService = app(\Core\Mod\Commerce\Services\CurrencyService::class);
$currencyService = app(CurrencyService::class);
return $currencyService->format($this->total, $this->display_currency);
}
@ -301,7 +306,7 @@ class Order extends Model
*/
public function getFormattedSubtotalAttribute(): string
{
$currencyService = app(\Core\Mod\Commerce\Services\CurrencyService::class);
$currencyService = app(CurrencyService::class);
return $currencyService->format($this->subtotal, $this->display_currency);
}
@ -311,7 +316,7 @@ class Order extends Model
*/
public function getFormattedTaxAmountAttribute(): string
{
$currencyService = app(\Core\Mod\Commerce\Services\CurrencyService::class);
$currencyService = app(CurrencyService::class);
return $currencyService->format($this->tax_amount, $this->display_currency);
}
@ -321,7 +326,7 @@ class Order extends Model
*/
public function getFormattedDiscountAmountAttribute(): string
{
$currencyService = app(\Core\Mod\Commerce\Services\CurrencyService::class);
$currencyService = app(CurrencyService::class);
return $currencyService->format($this->discount_amount, $this->display_currency);
}
@ -341,7 +346,7 @@ class Order extends Model
return $amount;
}
return \Core\Mod\Commerce\Models\ExchangeRate::convert(
return ExchangeRate::convert(
$amount,
$this->display_currency,
$baseCurrency
@ -363,7 +368,7 @@ class Order extends Model
return $amount;
}
return \Core\Mod\Commerce\Models\ExchangeRate::convert(
return ExchangeRate::convert(
$amount,
$baseCurrency,
$this->display_currency

View file

@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
namespace Core\Mod\Commerce\Models;
use Core\Tenant\Models\Package;

View file

@ -1,7 +1,10 @@
<?php
declare(strict_types=1);
namespace Core\Mod\Commerce\Models;
use Core\Mod\Commerce\Database\Factories\PaymentFactory;
use Core\Tenant\Models\Workspace;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
@ -33,9 +36,9 @@ class Payment extends Model
{
use HasFactory;
protected static function newFactory(): \Core\Mod\Commerce\Database\Factories\PaymentFactory
protected static function newFactory(): PaymentFactory
{
return \Core\Mod\Commerce\Database\Factories\PaymentFactory::new();
return PaymentFactory::new();
}
protected $fillable = [

View file

@ -1,7 +1,10 @@
<?php
declare(strict_types=1);
namespace Core\Mod\Commerce\Models;
use Carbon\Carbon;
use Core\Tenant\Models\User;
use Core\Tenant\Models\Workspace;
use Illuminate\Database\Eloquent\Factories\HasFactory;
@ -88,7 +91,7 @@ class PaymentMethod extends Model
return false;
}
$expiry = \Carbon\Carbon::createFromDate($this->exp_year, $this->exp_month)->endOfMonth();
$expiry = Carbon::createFromDate($this->exp_year, $this->exp_month)->endOfMonth();
return $expiry->isPast();
}

View file

@ -4,6 +4,7 @@ declare(strict_types=1);
namespace Core\Mod\Commerce\Models;
use Carbon\Carbon;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
@ -23,7 +24,7 @@ use Illuminate\Database\Eloquent\Relations\BelongsTo;
* @property bool $locked
* @property string $source
* @property int|null $set_by_entity_id
* @property \Carbon\Carbon|null $trained_at
* @property Carbon|null $trained_at
* @property string|null $trained_route
*/
class PermissionMatrix extends Model

View file

@ -4,6 +4,7 @@ declare(strict_types=1);
namespace Core\Mod\Commerce\Models;
use Carbon\Carbon;
use Core\Tenant\Models\User;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
@ -27,7 +28,7 @@ use Illuminate\Database\Eloquent\Relations\BelongsTo;
* @property int|null $user_id
* @property string $status
* @property bool $was_trained
* @property \Carbon\Carbon|null $trained_at
* @property Carbon|null $trained_at
*/
class PermissionRequest extends Model
{

View file

@ -5,6 +5,7 @@ declare(strict_types=1);
namespace Core\Mod\Commerce\Models;
use Core\Mod\Commerce\Concerns\HasContentOverrides;
use Core\Mod\Commerce\Services\CurrencyService;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
@ -287,7 +288,7 @@ class Product extends Model
public function formatPrice(int $amount, ?string $currency = null): string
{
$currency = $currency ?? $this->currency;
$currencyService = app(\Core\Mod\Commerce\Services\CurrencyService::class);
$currencyService = app(CurrencyService::class);
return $currencyService->format($amount, $currency, isCents: true);
}

View file

@ -4,6 +4,7 @@ declare(strict_types=1);
namespace Core\Mod\Commerce\Models;
use Carbon\Carbon;
use Core\Tenant\Models\User;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
@ -30,13 +31,13 @@ use Spatie\Activitylog\Traits\LogsActivity;
* @property string|null $ip_address
* @property string|null $user_agent
* @property string|null $tracking_id
* @property \Carbon\Carbon|null $clicked_at
* @property \Carbon\Carbon|null $signed_up_at
* @property \Carbon\Carbon|null $first_purchase_at
* @property \Carbon\Carbon|null $qualified_at
* @property \Carbon\Carbon|null $disqualified_at
* @property Carbon|null $clicked_at
* @property Carbon|null $signed_up_at
* @property Carbon|null $first_purchase_at
* @property Carbon|null $qualified_at
* @property Carbon|null $disqualified_at
* @property string|null $disqualification_reason
* @property \Carbon\Carbon|null $matured_at
* @property Carbon|null $matured_at
*/
class Referral extends Model
{

View file

@ -4,6 +4,7 @@ declare(strict_types=1);
namespace Core\Mod\Commerce\Models;
use Carbon\Carbon;
use Core\Tenant\Models\User;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
@ -24,8 +25,8 @@ use Spatie\Activitylog\Traits\LogsActivity;
* @property int $cookie_days
* @property int|null $max_uses
* @property int $uses_count
* @property \Carbon\Carbon|null $valid_from
* @property \Carbon\Carbon|null $valid_until
* @property Carbon|null $valid_from
* @property Carbon|null $valid_until
* @property bool $is_active
* @property string|null $campaign_name
* @property array|null $metadata

View file

@ -4,6 +4,7 @@ declare(strict_types=1);
namespace Core\Mod\Commerce\Models;
use Carbon\Carbon;
use Core\Tenant\Models\User;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
@ -26,10 +27,10 @@ use Spatie\Activitylog\Traits\LogsActivity;
* @property float $commission_amount
* @property string $currency
* @property string $status
* @property \Carbon\Carbon|null $matures_at
* @property \Carbon\Carbon|null $matured_at
* @property Carbon|null $matures_at
* @property Carbon|null $matured_at
* @property int|null $payout_id
* @property \Carbon\Carbon|null $paid_at
* @property Carbon|null $paid_at
* @property string|null $notes
*/
class ReferralCommission extends Model

View file

@ -4,6 +4,7 @@ declare(strict_types=1);
namespace Core\Mod\Commerce\Models;
use Carbon\Carbon;
use Core\Tenant\Models\User;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
@ -27,10 +28,10 @@ use Spatie\Activitylog\Traits\LogsActivity;
* @property float|null $btc_amount
* @property float|null $btc_rate
* @property string $status
* @property \Carbon\Carbon|null $requested_at
* @property \Carbon\Carbon|null $processed_at
* @property \Carbon\Carbon|null $completed_at
* @property \Carbon\Carbon|null $failed_at
* @property Carbon|null $requested_at
* @property Carbon|null $processed_at
* @property Carbon|null $completed_at
* @property Carbon|null $failed_at
* @property string|null $notes
* @property string|null $failure_reason
* @property int|null $processed_by

View file

@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
namespace Core\Mod\Commerce\Models;
use Core\Tenant\Models\User;

View file

@ -1,7 +1,11 @@
<?php
declare(strict_types=1);
namespace Core\Mod\Commerce\Models;
use Carbon\Carbon;
use Core\Mod\Commerce\Database\Factories\SubscriptionFactory;
use Core\Mod\Commerce\Events\SubscriptionCreated;
use Core\Mod\Commerce\Events\SubscriptionUpdated;
use Core\Tenant\Models\Workspace;
@ -26,12 +30,12 @@ use Spatie\Activitylog\Traits\LogsActivity;
* @property string $gateway_customer_id
* @property string|null $gateway_price_id
* @property string $status
* @property \Carbon\Carbon $current_period_start
* @property \Carbon\Carbon $current_period_end
* @property \Carbon\Carbon|null $trial_ends_at
* @property Carbon $current_period_start
* @property Carbon $current_period_end
* @property Carbon|null $trial_ends_at
* @property bool $cancel_at_period_end
* @property \Carbon\Carbon|null $cancelled_at
* @property \Carbon\Carbon|null $ended_at
* @property Carbon|null $cancelled_at
* @property Carbon|null $ended_at
* @property array|null $metadata
*/
class Subscription extends Model
@ -39,9 +43,9 @@ class Subscription extends Model
use HasFactory;
use LogsActivity;
protected static function newFactory(): \Core\Mod\Commerce\Database\Factories\SubscriptionFactory
protected static function newFactory(): SubscriptionFactory
{
return \Core\Mod\Commerce\Database\Factories\SubscriptionFactory::new();
return SubscriptionFactory::new();
}
/**
@ -231,7 +235,7 @@ class Subscription extends Model
$this->update(['status' => 'past_due']);
}
public function renew(\Carbon\Carbon $periodStart, \Carbon\Carbon $periodEnd): void
public function renew(Carbon $periodStart, Carbon $periodEnd): void
{
$this->update([
'status' => 'active',

View file

@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
namespace Core\Mod\Commerce\Models;
use Carbon\Carbon;
@ -13,10 +15,10 @@ use Illuminate\Database\Eloquent\Relations\BelongsTo;
* @property int $subscription_id
* @property int $meter_id
* @property int $quantity
* @property \Carbon\Carbon $period_start
* @property \Carbon\Carbon $period_end
* @property Carbon $period_start
* @property Carbon $period_end
* @property string|null $stripe_usage_record_id
* @property \Carbon\Carbon|null $synced_at
* @property Carbon|null $synced_at
* @property bool $billed
* @property int|null $invoice_item_id
* @property array|null $metadata

View file

@ -1,7 +1,10 @@
<?php
declare(strict_types=1);
namespace Core\Mod\Commerce\Models;
use Carbon\Carbon;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
@ -17,8 +20,8 @@ use Illuminate\Database\Eloquent\Model;
* @property string $type
* @property float $rate
* @property bool $is_digital_services
* @property \Carbon\Carbon $effective_from
* @property \Carbon\Carbon|null $effective_until
* @property Carbon $effective_from
* @property Carbon|null $effective_until
* @property bool $is_active
* @property string|null $stripe_tax_rate_id
*/

View file

@ -1,7 +1,10 @@
<?php
declare(strict_types=1);
namespace Core\Mod\Commerce\Models;
use Carbon\Carbon;
use Core\Tenant\Models\User;
use Core\Tenant\Models\Workspace;
use Illuminate\Database\Eloquent\Model;
@ -16,7 +19,7 @@ use Illuminate\Support\Str;
* @property int $meter_id
* @property int $workspace_id
* @property int $quantity
* @property \Carbon\Carbon $event_at
* @property Carbon $event_at
* @property string|null $idempotency_key
* @property int|null $user_id
* @property string|null $action

View file

@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
namespace Core\Mod\Commerce\Models;
use Illuminate\Database\Eloquent\Model;

View file

@ -4,6 +4,7 @@ declare(strict_types=1);
namespace Core\Mod\Commerce\Models;
use Carbon\Carbon;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
@ -21,8 +22,8 @@ use Illuminate\Database\Eloquent\Relations\BelongsTo;
* @property int|null $http_status_code
* @property int|null $order_id
* @property int|null $subscription_id
* @property \Carbon\Carbon $received_at
* @property \Carbon\Carbon|null $processed_at
* @property Carbon $received_at
* @property Carbon|null $processed_at
*/
class WebhookEvent extends Model
{

View file

@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
namespace Core\Mod\Commerce\Notifications;
use Core\Mod\Commerce\Models\Subscription;

View file

@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
namespace Core\Mod\Commerce\Notifications;
use Core\Mod\Commerce\Models\Order;

View file

@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
namespace Core\Mod\Commerce\Notifications;
use Core\Mod\Commerce\Models\Subscription;

View file

@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
namespace Core\Mod\Commerce\Notifications;
use Core\Mod\Commerce\Models\Invoice;

View file

@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
namespace Core\Mod\Commerce\Notifications;
use Core\Mod\Commerce\Models\Refund;

View file

@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
namespace Core\Mod\Commerce\Notifications;
use Core\Mod\Commerce\Models\Subscription;

View file

@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
namespace Core\Mod\Commerce\Notifications;
use Core\Mod\Commerce\Models\Subscription;

View file

@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
namespace Core\Mod\Commerce\Notifications;
use Core\Mod\Commerce\Models\Subscription;

View file

@ -6,6 +6,7 @@ namespace Core\Mod\Commerce\Services;
use Core\Mod\Commerce\Contracts\Orderable;
use Core\Mod\Commerce\Data\FraudAssessment;
use Core\Mod\Commerce\Events\OrderPaid;
use Core\Mod\Commerce\Exceptions\CheckoutRateLimitException;
use Core\Mod\Commerce\Exceptions\FraudBlockedException;
use Core\Mod\Commerce\Models\Coupon;
@ -13,9 +14,12 @@ use Core\Mod\Commerce\Models\Invoice;
use Core\Mod\Commerce\Models\Order;
use Core\Mod\Commerce\Models\OrderItem;
use Core\Mod\Commerce\Models\Payment;
use Core\Mod\Commerce\Models\Refund;
use Core\Mod\Commerce\Models\Subscription;
use Core\Mod\Commerce\Services\PaymentGateway\PaymentGatewayContract;
use Core\Tenant\Models\Boost;
use Core\Tenant\Models\Package;
use Core\Tenant\Models\User;
use Core\Tenant\Models\Workspace;
use Core\Tenant\Services\EntitlementService;
use Illuminate\Database\Eloquent\Model;
@ -123,7 +127,7 @@ class CommerceService
$order = Order::create([
'orderable_type' => get_class($orderable),
'orderable_id' => $orderable->id,
'user_id' => $orderable instanceof \Core\Tenant\Models\User ? $orderable->id : null,
'user_id' => $orderable instanceof User ? $orderable->id : null,
'order_number' => Order::generateOrderNumber(),
'status' => 'pending',
'billing_cycle' => $billingCycle,
@ -361,7 +365,7 @@ class CommerceService
$order = Order::create([
'orderable_type' => get_class($orderable),
'orderable_id' => $orderable->id,
'user_id' => $orderable instanceof \Core\Tenant\Models\User ? $orderable->id : null,
'user_id' => $orderable instanceof User ? $orderable->id : null,
'order_number' => Order::generateOrderNumber(),
'status' => 'pending',
'billing_cycle' => 'onetime',
@ -437,7 +441,7 @@ class CommerceService
}
// Provision boosts for user-level orders
if ($order->orderable instanceof \Core\Tenant\Models\User) {
if ($order->orderable instanceof User) {
foreach ($order->items as $item) {
if ($item->item_type === 'boost') {
$quantity = $item->metadata['quantity'] ?? $item->quantity ?? 1;
@ -450,28 +454,28 @@ class CommerceService
}
// Dispatch OrderPaid event for referral tracking and other listeners
event(new \Core\Mod\Commerce\Events\OrderPaid($order, $payment));
event(new OrderPaid($order, $payment));
});
}
/**
* Provision a boost for a user.
*/
public function provisionBoostForUser(\Core\Tenant\Models\User $user, string $featureCode, int $quantity = 1, array $metadata = []): \Core\Tenant\Models\Boost
public function provisionBoostForUser(User $user, string $featureCode, int $quantity = 1, array $metadata = []): Boost
{
// Use ADD_LIMIT for quantity-based boosts, ENABLE for boolean boosts
$boostType = $quantity > 1 || $this->isQuantityBasedFeature($featureCode)
? \Core\Tenant\Models\Boost::BOOST_TYPE_ADD_LIMIT
: \Core\Tenant\Models\Boost::BOOST_TYPE_ENABLE;
? Boost::BOOST_TYPE_ADD_LIMIT
: Boost::BOOST_TYPE_ENABLE;
return \Core\Tenant\Models\Boost::create([
return Boost::create([
'user_id' => $user->id,
'workspace_id' => null,
'feature_code' => $featureCode,
'boost_type' => $boostType,
'duration_type' => \Core\Tenant\Models\Boost::DURATION_PERMANENT,
'limit_value' => $boostType === \Core\Tenant\Models\Boost::BOOST_TYPE_ADD_LIMIT ? $quantity : null,
'status' => \Core\Tenant\Models\Boost::STATUS_ACTIVE,
'duration_type' => Boost::DURATION_PERMANENT,
'limit_value' => $boostType === Boost::BOOST_TYPE_ADD_LIMIT ? $quantity : null,
'status' => Boost::STATUS_ACTIVE,
'starts_at' => now(),
'metadata' => $metadata,
]);
@ -599,7 +603,7 @@ class CommerceService
Payment $payment,
?float $amount = null,
?string $reason = null
): \Core\Mod\Commerce\Models\Refund {
): Refund {
$amountCents = $amount
? (int) ($amount * 100)
: (int) (($payment->amount - $payment->amount_refunded) * 100);
@ -660,7 +664,7 @@ class CommerceService
// For BTCPay, payment will be 'pending' as it requires customer action
// This is expected - automatic retry won't work for crypto payments
if ($payment->status === 'pending' && $paymentMethod->gateway === 'btcpay') {
\Illuminate\Support\Facades\Log::info('BTCPay invoice created for retry - requires customer payment', [
Log::info('BTCPay invoice created for retry - requires customer payment', [
'invoice_id' => $invoice->id,
'payment_id' => $payment->id,
]);
@ -668,7 +672,7 @@ class CommerceService
return false;
} catch (\Exception $e) {
\Illuminate\Support\Facades\Log::error('Invoice payment retry failed', [
Log::error('Invoice payment retry failed', [
'invoice_id' => $invoice->id,
'error' => $e->getMessage(),
]);

View file

@ -11,6 +11,7 @@ use Core\Mod\Commerce\Models\CouponUsage;
use Core\Mod\Commerce\Models\Order;
use Core\Tenant\Models\Package;
use Core\Tenant\Models\Workspace;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Model;
/**
@ -217,7 +218,7 @@ class CouponService
/**
* Get usage history for a coupon.
*/
public function getUsageHistory(Coupon $coupon, int $limit = 50): \Illuminate\Database\Eloquent\Collection
public function getUsageHistory(Coupon $coupon, int $limit = 50): Collection
{
return $coupon->usages()
->with(['workspace', 'order'])

View file

@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
namespace Core\Mod\Commerce\Services;
use Core\Mod\Commerce\Models\CreditNote;

View file

@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
namespace Core\Mod\Commerce\Services;
use Carbon\Carbon;

View file

@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
namespace Core\Mod\Commerce\Services;
use Barryvdh\DomPDF\Facade\Pdf;
@ -9,8 +11,11 @@ use Core\Mod\Commerce\Models\InvoiceItem;
use Core\Mod\Commerce\Models\Order;
use Core\Mod\Commerce\Models\Payment;
use Core\Tenant\Models\Workspace;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Pagination\LengthAwarePaginator;
use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Facades\Storage;
use Symfony\Component\HttpFoundation\StreamedResponse;
/**
* Invoice generation and management service.
@ -174,7 +179,7 @@ class InvoiceService
/**
* Get PDF download response.
*/
public function downloadPdf(Invoice $invoice): \Symfony\Component\HttpFoundation\StreamedResponse
public function downloadPdf(Invoice $invoice): StreamedResponse
{
$path = $this->getPdf($invoice);
@ -219,7 +224,7 @@ class InvoiceService
/**
* Get invoices for a workspace.
*/
public function getForWorkspace(Workspace $workspace, int $limit = 25): \Illuminate\Pagination\LengthAwarePaginator
public function getForWorkspace(Workspace $workspace, int $limit = 25): LengthAwarePaginator
{
return $workspace->invoices()
->with('items')
@ -230,7 +235,7 @@ class InvoiceService
/**
* Get unpaid invoices for a workspace.
*/
public function getUnpaidForWorkspace(Workspace $workspace): \Illuminate\Database\Eloquent\Collection
public function getUnpaidForWorkspace(Workspace $workspace): Collection
{
return $workspace->invoices()
->pending()
@ -241,7 +246,7 @@ class InvoiceService
/**
* Get overdue invoices for a workspace.
*/
public function getOverdueForWorkspace(Workspace $workspace): \Illuminate\Database\Eloquent\Collection
public function getOverdueForWorkspace(Workspace $workspace): Collection
{
return $workspace->invoices()
->pending()

View file

@ -10,6 +10,7 @@ use Core\Mod\Commerce\Models\Payment;
use Core\Mod\Commerce\Models\PaymentMethod;
use Core\Mod\Commerce\Models\Subscription;
use Core\Tenant\Models\Workspace;
use Illuminate\Http\Client\Response;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Str;
@ -633,7 +634,7 @@ class BTCPayGateway implements PaymentGatewayContract
/**
* Extract a safe error message from a failed response.
*/
protected function sanitiseErrorMessage(\Illuminate\Http\Client\Response $response): string
protected function sanitiseErrorMessage(Response $response): string
{
$json = $response->json();

View file

@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
namespace Core\Mod\Commerce\Services\PaymentGateway;
use Core\Mod\Commerce\Models\Order;

View file

@ -1,7 +1,10 @@
<?php
declare(strict_types=1);
namespace Core\Mod\Commerce\Services\PaymentGateway;
use Carbon\Carbon;
use Core\Mod\Commerce\Models\Order;
use Core\Mod\Commerce\Models\Payment;
use Core\Mod\Commerce\Models\PaymentMethod;
@ -9,7 +12,14 @@ use Core\Mod\Commerce\Models\Refund;
use Core\Mod\Commerce\Models\Subscription;
use Core\Tenant\Models\Workspace;
use Illuminate\Support\Facades\Log;
use Stripe\Exception\ApiConnectionException;
use Stripe\Exception\ApiErrorException;
use Stripe\Exception\AuthenticationException;
use Stripe\Exception\CardException;
use Stripe\Exception\InvalidRequestException;
use Stripe\Exception\RateLimitException;
use Stripe\StripeClient;
use Stripe\Webhook;
/**
* Stripe payment gateway implementation.
@ -143,36 +153,36 @@ class StripeGateway implements PaymentGatewayContract
'session_id' => $session->id,
'checkout_url' => $session->url,
];
} catch (\Stripe\Exception\CardException $e) {
} catch (CardException $e) {
Log::warning('Stripe checkout failed: card error', [
'order_id' => $order->id,
'error' => $e->getMessage(),
'code' => $e->getStripeCode(),
]);
throw new \RuntimeException('Payment card error: '.$e->getMessage(), 0, $e);
} catch (\Stripe\Exception\RateLimitException $e) {
} catch (RateLimitException $e) {
Log::error('Stripe checkout failed: rate limit', [
'order_id' => $order->id,
]);
throw new \RuntimeException('Payment service temporarily unavailable. Please try again.', 0, $e);
} catch (\Stripe\Exception\InvalidRequestException $e) {
} catch (InvalidRequestException $e) {
Log::error('Stripe checkout failed: invalid request', [
'order_id' => $order->id,
'error' => $e->getMessage(),
'param' => $e->getStripeParam(),
]);
throw new \RuntimeException('Unable to create checkout session. Please contact support.', 0, $e);
} catch (\Stripe\Exception\AuthenticationException $e) {
} catch (AuthenticationException $e) {
Log::critical('Stripe authentication failed - check API keys', [
'order_id' => $order->id,
]);
throw new \RuntimeException('Payment service configuration error. Please contact support.', 0, $e);
} catch (\Stripe\Exception\ApiConnectionException $e) {
} catch (ApiConnectionException $e) {
Log::error('Stripe checkout failed: connection error', [
'order_id' => $order->id,
]);
throw new \RuntimeException('Unable to connect to payment service. Please try again.', 0, $e);
} catch (\Stripe\Exception\ApiErrorException $e) {
} catch (ApiErrorException $e) {
Log::error('Stripe checkout failed: API error', [
'order_id' => $order->id,
'error' => $e->getMessage(),
@ -338,10 +348,10 @@ class StripeGateway implements PaymentGatewayContract
'gateway_customer_id' => $customerId,
'gateway_price_id' => $priceId,
'status' => $this->mapSubscriptionStatus($stripeSubscription->status),
'current_period_start' => \Carbon\Carbon::createFromTimestamp($stripeSubscription->current_period_start),
'current_period_end' => \Carbon\Carbon::createFromTimestamp($stripeSubscription->current_period_end),
'current_period_start' => Carbon::createFromTimestamp($stripeSubscription->current_period_start),
'current_period_end' => Carbon::createFromTimestamp($stripeSubscription->current_period_end),
'trial_ends_at' => $stripeSubscription->trial_end
? \Carbon\Carbon::createFromTimestamp($stripeSubscription->trial_end)
? Carbon::createFromTimestamp($stripeSubscription->trial_end)
: null,
'metadata' => ['stripe_subscription' => $stripeSubscription->toArray()],
]);
@ -376,8 +386,8 @@ class StripeGateway implements PaymentGatewayContract
'gateway_price_id' => $options['price_id'] ?? $subscription->gateway_price_id,
'status' => $this->mapSubscriptionStatus($stripeSubscription->status),
'cancel_at_period_end' => $stripeSubscription->cancel_at_period_end,
'current_period_start' => \Carbon\Carbon::createFromTimestamp($stripeSubscription->current_period_start),
'current_period_end' => \Carbon\Carbon::createFromTimestamp($stripeSubscription->current_period_end),
'current_period_start' => Carbon::createFromTimestamp($stripeSubscription->current_period_start),
'current_period_end' => Carbon::createFromTimestamp($stripeSubscription->current_period_end),
]);
return $subscription->fresh();
@ -544,7 +554,7 @@ class StripeGateway implements PaymentGatewayContract
public function verifyWebhookSignature(string $payload, string $signature): bool
{
try {
\Stripe\Webhook::constructEvent($payload, $signature, $this->webhookSecret);
Webhook::constructEvent($payload, $signature, $this->webhookSecret);
return true;
} catch (\Exception $e) {

View file

@ -1,7 +1,10 @@
<?php
declare(strict_types=1);
namespace Core\Mod\Commerce\Services;
use Carbon\Carbon;
use Core\Mod\Commerce\Models\PaymentMethod;
use Core\Mod\Commerce\Services\PaymentGateway\StripeGateway;
use Core\Tenant\Models\User;
@ -253,7 +256,7 @@ class PaymentMethodService
return false;
}
$expiry = \Carbon\Carbon::createFromDate(
$expiry = Carbon::createFromDate(
$paymentMethod->exp_year,
$paymentMethod->exp_month
)->endOfMonth();

View file

@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
namespace Core\Mod\Commerce\Services;
/**

View file

@ -1,11 +1,14 @@
<?php
declare(strict_types=1);
namespace Core\Mod\Commerce\Services;
use Core\Mod\Commerce\Models\Payment;
use Core\Mod\Commerce\Models\Refund;
use Core\Mod\Commerce\Notifications\RefundProcessed;
use Core\Tenant\Models\User;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
@ -160,7 +163,7 @@ class RefundService
/**
* Get refund history for a payment.
*/
public function getRefundsForPayment(Payment $payment): \Illuminate\Database\Eloquent\Collection
public function getRefundsForPayment(Payment $payment): Collection
{
return $payment->refunds()->latest()->get();
}
@ -168,7 +171,7 @@ class RefundService
/**
* Get all refunds for a workspace.
*/
public function getRefundsForWorkspace(int $workspaceId): \Illuminate\Database\Eloquent\Collection
public function getRefundsForWorkspace(int $workspaceId): Collection
{
return Refund::query()
->whereHas('payment', function ($query) use ($workspaceId) {

View file

@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
namespace Core\Mod\Commerce\Services;
use Carbon\Carbon;
@ -9,6 +11,7 @@ use Core\Tenant\Models\Package;
use Core\Tenant\Models\Workspace;
use Core\Tenant\Models\WorkspacePackage;
use Core\Tenant\Services\EntitlementService;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
@ -414,7 +417,7 @@ class SubscriptionService
/**
* Get subscriptions expiring soon (for renewal reminders).
*/
public function getExpiringSoon(int $days = 7): \Illuminate\Database\Eloquent\Collection
public function getExpiringSoon(int $days = 7): Collection
{
return Subscription::query()
->active()
@ -428,7 +431,7 @@ class SubscriptionService
/**
* Get subscriptions that have failed payment and need dunning.
*/
public function getFailedPayments(): \Illuminate\Database\Eloquent\Collection
public function getFailedPayments(): Collection
{
return Subscription::query()
->where('status', 'past_due')

View file

@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
namespace Core\Mod\Commerce\Services;
use Core\Mod\Commerce\Contracts\Orderable;

View file

@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
namespace Core\Mod\Commerce\Services;
use Carbon\Carbon;
@ -15,6 +17,7 @@ use Core\Tenant\Models\Workspace;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
use Stripe\StripeClient;
/**
* Usage-based billing service.
@ -331,7 +334,7 @@ class UsageBillingService
Subscription $subscription,
SubscriptionUsage $usage
): void {
$stripe = new \Stripe\StripeClient(config('commerce.gateways.stripe.secret'));
$stripe = new StripeClient(config('commerce.gateways.stripe.secret'));
// Find the subscription item for this price
$stripeSubscription = $stripe->subscriptions->retrieve(
@ -417,7 +420,7 @@ class UsageBillingService
return null;
}
$stripe = new \Stripe\StripeClient($secret);
$stripe = new StripeClient($secret);
// Create or update product in Stripe
$product = $stripe->products->create([

View file

@ -7,6 +7,7 @@ namespace Core\Mod\Commerce\Services;
use Core\Mod\Commerce\Models\Order;
use Core\Mod\Commerce\Models\Subscription;
use Core\Mod\Commerce\Models\WebhookEvent;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\QueryException;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
@ -311,7 +312,7 @@ class WebhookLogger
/**
* Get recent failed events for debugging.
*/
public function getRecentFailures(string $gateway, int $limit = 10): \Illuminate\Database\Eloquent\Collection
public function getRecentFailures(string $gateway, int $limit = 10): Collection
{
return WebhookEvent::forGateway($gateway)
->failed()

View file

@ -4,9 +4,11 @@ declare(strict_types=1);
namespace Core\Mod\Commerce\View\Modal\Admin;
use Carbon\Carbon;
use Core\Mod\Commerce\Models\Coupon;
use Core\Mod\Commerce\Services\CouponService;
use Core\Tenant\Models\Package;
use Illuminate\Support\Str;
use Livewire\Attributes\Computed;
use Livewire\Attributes\Layout;
use Livewire\Attributes\Title;
@ -334,8 +336,8 @@ class CouponManager extends Component
'max_uses_per_workspace' => $this->bulk_max_uses_per_workspace,
'duration' => $this->bulk_duration,
'duration_months' => $this->bulk_duration === 'repeating' ? $this->bulk_duration_months : null,
'valid_from' => $this->bulk_valid_from ? \Carbon\Carbon::parse($this->bulk_valid_from) : null,
'valid_until' => $this->bulk_valid_until ? \Carbon\Carbon::parse($this->bulk_valid_until) : null,
'valid_from' => $this->bulk_valid_from ? Carbon::parse($this->bulk_valid_from) : null,
'valid_until' => $this->bulk_valid_until ? Carbon::parse($this->bulk_valid_until) : null,
'is_active' => $this->bulk_is_active,
];
@ -399,8 +401,8 @@ class CouponManager extends Component
'max_uses_per_workspace' => $this->max_uses_per_workspace,
'duration' => $this->duration,
'duration_months' => $this->duration === 'repeating' ? $this->duration_months : null,
'valid_from' => $this->valid_from ? \Carbon\Carbon::parse($this->valid_from) : null,
'valid_until' => $this->valid_until ? \Carbon\Carbon::parse($this->valid_until) : null,
'valid_from' => $this->valid_from ? Carbon::parse($this->valid_from) : null,
'valid_until' => $this->valid_until ? Carbon::parse($this->valid_until) : null,
'is_active' => $this->is_active,
];
@ -587,7 +589,7 @@ class CouponManager extends Component
[
'lines' => array_filter([
['bold' => $c->name],
$c->description ? ['muted' => \Illuminate\Support\Str::limit($c->description, 30)] : null,
$c->description ? ['muted' => Str::limit($c->description, 30)] : null,
]),
],
['lines' => $discountLines],

View file

@ -8,6 +8,7 @@ use Core\Mod\Commerce\Models\Coupon;
use Core\Mod\Commerce\Models\Order;
use Core\Mod\Commerce\Models\Subscription;
use Illuminate\Contracts\View\View;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Support\Facades\DB;
use Livewire\Attributes\Computed;
use Livewire\Attributes\Title;
@ -42,7 +43,7 @@ class Dashboard extends Component
}
#[Computed]
public function recentOrders(): \Illuminate\Database\Eloquent\Collection
public function recentOrders(): Collection
{
return Order::with('workspace')
->latest()

View file

@ -4,6 +4,7 @@ declare(strict_types=1);
namespace Core\Mod\Commerce\View\Modal\Admin;
use Carbon\Carbon;
use Core\Mod\Commerce\Models\Referral;
use Core\Mod\Commerce\Models\ReferralCode;
use Core\Mod\Commerce\Models\ReferralCommission;
@ -309,8 +310,8 @@ class ReferralManager extends Component
'commission_rate' => $this->codeCommissionRate,
'cookie_days' => $this->codeCookieDays,
'max_uses' => $this->codeMaxUses,
'valid_from' => $this->codeValidFrom ? \Carbon\Carbon::parse($this->codeValidFrom) : null,
'valid_until' => $this->codeValidUntil ? \Carbon\Carbon::parse($this->codeValidUntil) : null,
'valid_from' => $this->codeValidFrom ? Carbon::parse($this->codeValidFrom) : null,
'valid_until' => $this->codeValidUntil ? Carbon::parse($this->codeValidUntil) : null,
'is_active' => $this->codeIsActive,
'campaign_name' => $this->codeCampaignName,
];

View file

@ -7,6 +7,7 @@ namespace Core\Mod\Commerce\View\Modal\Admin;
use Core\Mod\Commerce\Models\Subscription;
use Core\Mod\Commerce\Services\SubscriptionService;
use Core\Tenant\Models\Workspace;
use Illuminate\Support\Facades\DB;
use Livewire\Attributes\Computed;
use Livewire\Attributes\Title;
use Livewire\Component;
@ -150,7 +151,7 @@ class SubscriptionManager extends Component
$count = Subscription::whereIn('id', $this->selected)
->whereNotNull('current_period_end')
->update([
'current_period_end' => \Illuminate\Support\Facades\DB::raw('DATE_ADD(current_period_end, INTERVAL 30 DAY)'),
'current_period_end' => DB::raw('DATE_ADD(current_period_end, INTERVAL 30 DAY)'),
]);
session()->flash('message', __('commerce::commerce.bulk.period_extended', ['count' => $count, 'days' => 30]));

View file

@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
namespace Core\Mod\Commerce\View\Modal\Web;
use Core\Mod\Commerce\Models\Subscription;

View file

@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
namespace Core\Mod\Commerce\View\Modal\Web;
use Core\Mod\Commerce\Models\Order;

View file

@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
namespace Core\Mod\Commerce\View\Modal\Web;
use Core\Mod\Commerce\Models\Coupon;
@ -12,9 +14,11 @@ use Core\Mod\Commerce\Services\CurrencyService;
use Core\Mod\Commerce\Services\TaxService;
use Core\Tenant\Models\Package;
use Core\Tenant\Models\Workspace;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Support\Facades\Auth;
use Livewire\Attributes\Computed;
use Livewire\Attributes\Layout;
use Livewire\Attributes\On;
use Livewire\Attributes\Url;
use Livewire\Component;
@ -143,7 +147,7 @@ class CheckoutPage extends Component
/**
* Handle currency change event from CurrencySelector component.
*/
#[\Livewire\Attributes\On('currency-changed')]
#[On('currency-changed')]
public function onCurrencyChanged(string $currency): void
{
$this->displayCurrency = $currency;
@ -181,7 +185,7 @@ class CheckoutPage extends Component
}
#[Computed]
public function packages(): \Illuminate\Database\Eloquent\Collection
public function packages(): Collection
{
return Package::active()
->public()

View file

@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
namespace Core\Mod\Commerce\View\Modal\Web;
use Core\Mod\Commerce\Models\Order;
@ -9,6 +11,7 @@ use Illuminate\Auth\Events\Registered;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Str;
use Livewire\Attributes\Layout;
use Livewire\Attributes\Validate;
use Livewire\Component;
@ -156,9 +159,9 @@ class CheckoutSuccess extends Component
*/
protected function generateUniqueSlug(string $name): string
{
$baseSlug = \Illuminate\Support\Str::slug($name);
$baseSlug = Str::slug($name);
if (str_contains($baseSlug, '@')) {
$baseSlug = \Illuminate\Support\Str::slug(\Illuminate\Support\Str::before($name, '@'));
$baseSlug = Str::slug(Str::before($name, '@'));
}
$slug = $baseSlug;

View file

@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
namespace Core\Mod\Commerce\View\Modal\Web;
use Core\Mod\Commerce\Models\Subscription;

View file

@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
namespace Core\Mod\Commerce\View\Modal\Web;
use Core\Mod\Commerce\Services\CommerceService;

View file

@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
namespace Core\Mod\Commerce\View\Modal\Web;
use Core\Mod\Commerce\Models\PaymentMethod;

View file

@ -1,11 +1,14 @@
<?php
declare(strict_types=1);
namespace Core\Mod\Commerce\View\Modal\Web;
use Core\Mod\Commerce\Models\Subscription as SubscriptionModel;
use Core\Mod\Commerce\Notifications\SubscriptionCancelled;
use Core\Mod\Commerce\Services\CommerceService;
use Core\Mod\Commerce\Services\SubscriptionService;
use Core\Tenant\Models\User;
use Core\Tenant\Models\Workspace;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Auth;
@ -123,7 +126,7 @@ class Subscription extends Component
// Notify user
$user = Auth::user();
if ($user instanceof \Core\Tenant\Models\User) {
if ($user instanceof User) {
$user->notify(new SubscriptionCancelled($this->activeSubscription));
}

View file

@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
namespace Core\Mod\Commerce\View\Modal\Web;
use Core\Mod\Commerce\Models\Subscription;

View file

@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
return [
/*

View file

@ -1,5 +1,22 @@
<?php
declare(strict_types=1);
use Core\Mod\Commerce\Controllers\InvoiceController;
use Core\Mod\Commerce\View\Modal\Admin\CouponManager;
use Core\Mod\Commerce\View\Modal\Admin\CreditNoteManager;
use Core\Mod\Commerce\View\Modal\Admin\EntityManager;
use Core\Mod\Commerce\View\Modal\Admin\OrderManager;
use Core\Mod\Commerce\View\Modal\Admin\PermissionMatrixManager;
use Core\Mod\Commerce\View\Modal\Admin\ProductManager;
use Core\Mod\Commerce\View\Modal\Admin\ReferralManager;
use Core\Mod\Commerce\View\Modal\Admin\SubscriptionManager;
use Core\Mod\Commerce\View\Modal\Web\ChangePlan;
use Core\Mod\Commerce\View\Modal\Web\Dashboard;
use Core\Mod\Commerce\View\Modal\Web\Invoices;
use Core\Mod\Commerce\View\Modal\Web\PaymentMethods;
use Core\Mod\Commerce\View\Modal\Web\ReferralDashboard;
use Core\Mod\Commerce\View\Modal\Web\Subscription;
use Illuminate\Support\Facades\Route;
/*
@ -10,25 +27,25 @@ use Illuminate\Support\Facades\Route;
// Billing (user-facing hub pages)
Route::prefix('hub/billing')->name('hub.billing.')->group(function () {
Route::get('/', \Core\Mod\Commerce\View\Modal\Web\Dashboard::class)->name('index');
Route::get('/invoices', \Core\Mod\Commerce\View\Modal\Web\Invoices::class)->name('invoices');
Route::get('/invoices/{invoice}/pdf', [\Core\Mod\Commerce\Controllers\InvoiceController::class, 'pdf'])->name('invoices.pdf');
Route::get('/invoices/{invoice}/view', [\Core\Mod\Commerce\Controllers\InvoiceController::class, 'view'])->name('invoices.view');
Route::get('/payment-methods', \Core\Mod\Commerce\View\Modal\Web\PaymentMethods::class)->name('payment-methods');
Route::get('/subscription', \Core\Mod\Commerce\View\Modal\Web\Subscription::class)->name('subscription');
Route::get('/change-plan', \Core\Mod\Commerce\View\Modal\Web\ChangePlan::class)->name('change-plan');
Route::get('/affiliates', \Core\Mod\Commerce\View\Modal\Web\ReferralDashboard::class)->name('affiliates');
Route::get('/', Dashboard::class)->name('index');
Route::get('/invoices', Invoices::class)->name('invoices');
Route::get('/invoices/{invoice}/pdf', [InvoiceController::class, 'pdf'])->name('invoices.pdf');
Route::get('/invoices/{invoice}/view', [InvoiceController::class, 'view'])->name('invoices.view');
Route::get('/payment-methods', PaymentMethods::class)->name('payment-methods');
Route::get('/subscription', Subscription::class)->name('subscription');
Route::get('/change-plan', ChangePlan::class)->name('change-plan');
Route::get('/affiliates', ReferralDashboard::class)->name('affiliates');
});
// Commerce management (admin only - Hades tier)
Route::prefix('hub/commerce')->name('hub.commerce.')->group(function () {
Route::get('/', \Core\Mod\Commerce\View\Modal\Admin\Dashboard::class)->name('dashboard');
Route::get('/orders', \Core\Mod\Commerce\View\Modal\Admin\OrderManager::class)->name('orders');
Route::get('/subscriptions', \Core\Mod\Commerce\View\Modal\Admin\SubscriptionManager::class)->name('subscriptions');
Route::get('/coupons', \Core\Mod\Commerce\View\Modal\Admin\CouponManager::class)->name('coupons');
Route::get('/entities', \Core\Mod\Commerce\View\Modal\Admin\EntityManager::class)->name('entities');
Route::get('/permissions', \Core\Mod\Commerce\View\Modal\Admin\PermissionMatrixManager::class)->name('permissions');
Route::get('/products', \Core\Mod\Commerce\View\Modal\Admin\ProductManager::class)->name('products');
Route::get('/credit-notes', \Core\Mod\Commerce\View\Modal\Admin\CreditNoteManager::class)->name('credit-notes');
Route::get('/referrals', \Core\Mod\Commerce\View\Modal\Admin\ReferralManager::class)->name('referrals');
Route::get('/', Core\Mod\Commerce\View\Modal\Admin\Dashboard::class)->name('dashboard');
Route::get('/orders', OrderManager::class)->name('orders');
Route::get('/subscriptions', SubscriptionManager::class)->name('subscriptions');
Route::get('/coupons', CouponManager::class)->name('coupons');
Route::get('/entities', EntityManager::class)->name('entities');
Route::get('/permissions', PermissionMatrixManager::class)->name('permissions');
Route::get('/products', ProductManager::class)->name('products');
Route::get('/credit-notes', CreditNoteManager::class)->name('credit-notes');
Route::get('/referrals', ReferralManager::class)->name('referrals');
});

View file

@ -1,3 +1,5 @@
<?php
declare(strict_types=1);
// Console commands are registered via Core modules

View file

@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
use Core\Mod\Commerce\Models\Invoice;
use Core\Mod\Commerce\Models\Order;
use Core\Mod\Commerce\Models\Payment;
@ -9,9 +11,10 @@ use Core\Tenant\Models\Package;
use Core\Tenant\Models\User;
use Core\Tenant\Models\Workspace;
use Core\Tenant\Models\WorkspacePackage;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Cache;
uses(\Illuminate\Foundation\Testing\RefreshDatabase::class);
uses(RefreshDatabase::class);
beforeEach(function () {
Cache::flush();

View file

@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
use Core\Mod\Commerce\Models\Coupon;
use Core\Mod\Commerce\Models\CouponUsage;
use Core\Mod\Commerce\Models\Order;
@ -7,8 +9,9 @@ use Core\Mod\Commerce\Services\CouponService;
use Core\Tenant\Models\Package;
use Core\Tenant\Models\User;
use Core\Tenant\Models\Workspace;
use Illuminate\Foundation\Testing\RefreshDatabase;
uses(\Illuminate\Foundation\Testing\RefreshDatabase::class);
uses(RefreshDatabase::class);
beforeEach(function () {
$this->user = User::factory()->create();

View file

@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
use Carbon\Carbon;
use Core\Mod\Commerce\Models\Invoice;
use Core\Mod\Commerce\Models\Subscription;
@ -12,10 +14,11 @@ use Core\Tenant\Models\Package;
use Core\Tenant\Models\User;
use Core\Tenant\Models\Workspace;
use Core\Tenant\Models\WorkspacePackage;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Notification;
uses(\Illuminate\Foundation\Testing\RefreshDatabase::class);
uses(RefreshDatabase::class);
beforeEach(function () {
Cache::flush();

View file

@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
use Core\Mod\Commerce\Events\SubscriptionRenewed;
use Core\Mod\Commerce\Jobs\ProcessSubscriptionRenewal;
use Core\Mod\Commerce\Models\Subscription;
@ -11,10 +13,11 @@ use Core\Tenant\Models\User;
use Core\Tenant\Models\Workspace;
use Core\Tenant\Models\WorkspacePackage;
use Core\Tenant\Services\EntitlementService;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Event;
uses(\Illuminate\Foundation\Testing\RefreshDatabase::class);
uses(RefreshDatabase::class);
beforeEach(function () {
Cache::flush();

View file

@ -1,15 +1,19 @@
<?php
declare(strict_types=1);
use Core\Mod\Commerce\Models\Payment;
use Core\Mod\Commerce\Models\Refund;
use Core\Mod\Commerce\Notifications\RefundProcessed;
use Core\Mod\Commerce\Services\CommerceService;
use Core\Mod\Commerce\Services\PaymentGateway\PaymentGatewayContract;
use Core\Mod\Commerce\Services\RefundService;
use Core\Tenant\Models\User;
use Core\Tenant\Models\Workspace;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Notification;
uses(\Illuminate\Foundation\Testing\RefreshDatabase::class);
uses(RefreshDatabase::class);
beforeEach(function () {
Notification::fake();
@ -36,7 +40,7 @@ beforeEach(function () {
]);
// Mock the gateway
$mockGateway = Mockery::mock(\Core\Mod\Commerce\Services\PaymentGateway\PaymentGatewayContract::class);
$mockGateway = Mockery::mock(PaymentGatewayContract::class);
$mockGateway->shouldReceive('refund')->andReturn([
'success' => true,
'refund_id' => 're_test_123',
@ -92,15 +96,15 @@ describe('RefundService', function () {
it('throws exception for refund exceeding available amount', function () {
expect(fn () => $this->service->refund($this->payment, 150.00))
->toThrow(\InvalidArgumentException::class, 'exceeds maximum refundable');
->toThrow(InvalidArgumentException::class, 'exceeds maximum refundable');
});
it('throws exception for zero or negative amount', function () {
expect(fn () => $this->service->refund($this->payment, 0))
->toThrow(\InvalidArgumentException::class, 'greater than zero');
->toThrow(InvalidArgumentException::class, 'greater than zero');
expect(fn () => $this->service->refund($this->payment, -50.00))
->toThrow(\InvalidArgumentException::class, 'greater than zero');
->toThrow(InvalidArgumentException::class, 'greater than zero');
});
it('throws exception for non-succeeded payments', function () {
@ -114,7 +118,7 @@ describe('RefundService', function () {
]);
expect(fn () => $this->service->refund($pendingPayment, 50.00))
->toThrow(\InvalidArgumentException::class, 'only refund successful payments');
->toThrow(InvalidArgumentException::class, 'only refund successful payments');
});
it('allows multiple partial refunds up to full amount', function () {

View file

@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
use Carbon\Carbon;
use Core\Mod\Commerce\Exceptions\PauseLimitExceededException;
use Core\Mod\Commerce\Models\Subscription;
@ -10,9 +12,10 @@ use Core\Tenant\Models\Package;
use Core\Tenant\Models\User;
use Core\Tenant\Models\Workspace;
use Core\Tenant\Models\WorkspacePackage;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Cache;
uses(\Illuminate\Foundation\Testing\RefreshDatabase::class);
uses(RefreshDatabase::class);
beforeEach(function () {
Cache::flush();
@ -462,7 +465,7 @@ describe('Proration calculations', function () {
$this->subscription->workspacePackage->setRelation('package', null);
expect(fn () => $this->service->previewPlanChange($this->subscription, $this->agencyPackage))
->toThrow(\InvalidArgumentException::class, 'no current package');
->toThrow(InvalidArgumentException::class, 'no current package');
});
});

View file

@ -1,10 +1,13 @@
<?php
declare(strict_types=1);
use Core\Mod\Commerce\Models\TaxRate;
use Core\Mod\Commerce\Services\TaxService;
use Core\Tenant\Models\Workspace;
use Illuminate\Foundation\Testing\RefreshDatabase;
uses(\Illuminate\Foundation\Testing\RefreshDatabase::class);
uses(RefreshDatabase::class);
beforeEach(function () {
$this->workspace = Workspace::factory()->create([

View file

@ -4,9 +4,10 @@ declare(strict_types=1);
use Core\Mod\Commerce\Services\WebhookRateLimiter;
use Illuminate\Cache\RateLimiter;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Http\Request;
uses(\Illuminate\Foundation\Testing\RefreshDatabase::class);
uses(RefreshDatabase::class);
// ============================================================================
// WebhookRateLimiter Unit Tests

View file

@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
use Core\Mod\Commerce\Controllers\Webhooks\BTCPayWebhookController;
use Core\Mod\Commerce\Controllers\Webhooks\StripeWebhookController;
use Core\Mod\Commerce\Models\Order;
@ -19,10 +21,12 @@ use Core\Tenant\Models\User;
use Core\Tenant\Models\Workspace;
use Core\Tenant\Models\WorkspacePackage;
use Core\Tenant\Services\EntitlementService;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Notification;
use WebhookPayloadValidationException;
uses(\Illuminate\Foundation\Testing\RefreshDatabase::class);
uses(RefreshDatabase::class);
beforeEach(function () {
Notification::fake();
@ -124,7 +128,7 @@ describe('StripeWebhookController', function () {
$webhookLogger
);
$request = new \Illuminate\Http\Request;
$request = new Request;
$request->headers->set('Stripe-Signature', 't=123,v1=abc');
$response = $controller->handle($request);
@ -177,7 +181,7 @@ describe('StripeWebhookController', function () {
$webhookLogger
);
$request = new \Illuminate\Http\Request;
$request = new Request;
$response = $controller->handle($request);
expect($response->getStatusCode())->toBe(200)
@ -213,7 +217,7 @@ describe('StripeWebhookController', function () {
$webhookLogger
);
$request = new \Illuminate\Http\Request;
$request = new Request;
$response = $controller->handle($request);
expect($response->getStatusCode())->toBe(200)
@ -270,7 +274,7 @@ describe('StripeWebhookController', function () {
$webhookLogger
);
$request = new \Illuminate\Http\Request;
$request = new Request;
$response = $controller->handle($request);
expect($response->getStatusCode())->toBe(200);
@ -332,7 +336,7 @@ describe('StripeWebhookController', function () {
$webhookLogger
);
$request = new \Illuminate\Http\Request;
$request = new Request;
$response = $controller->handle($request);
expect($response->getStatusCode())->toBe(200);
@ -368,7 +372,7 @@ describe('StripeWebhookController', function () {
$webhookLogger
);
$request = new \Illuminate\Http\Request;
$request = new Request;
$response = $controller->handle($request);
expect($response->getStatusCode())->toBe(200)
@ -460,7 +464,7 @@ describe('BTCPayWebhookController', function () {
$controller = new BTCPayWebhookController($mockGateway, $mockCommerce, $webhookLogger);
$request = new \Illuminate\Http\Request;
$request = new Request;
$request->headers->set('BTCPay-Sig', 'valid_signature');
$response = $controller->handle($request);
@ -500,7 +504,7 @@ describe('BTCPayWebhookController', function () {
$controller = new BTCPayWebhookController($mockGateway, $mockCommerce, $webhookLogger);
$request = new \Illuminate\Http\Request;
$request = new Request;
$response = $controller->handle($request);
expect($response->getStatusCode())->toBe(200)
@ -523,7 +527,7 @@ describe('BTCPayWebhookController', function () {
$controller = new BTCPayWebhookController($mockGateway, $mockCommerce, $webhookLogger);
$request = new \Illuminate\Http\Request;
$request = new Request;
$response = $controller->handle($request);
expect($response->getStatusCode())->toBe(200)
@ -548,7 +552,7 @@ describe('BTCPayWebhookController', function () {
$controller = new BTCPayWebhookController($mockGateway, $mockCommerce, $webhookLogger);
$request = new \Illuminate\Http\Request;
$request = new Request;
$response = $controller->handle($request);
expect($response->getStatusCode())->toBe(200);
@ -575,7 +579,7 @@ describe('BTCPayWebhookController', function () {
$controller = new BTCPayWebhookController($mockGateway, $mockCommerce, $webhookLogger);
$request = new \Illuminate\Http\Request;
$request = new Request;
$response = $controller->handle($request);
expect($response->getStatusCode())->toBe(200);
@ -602,7 +606,7 @@ describe('BTCPayWebhookController', function () {
$controller = new BTCPayWebhookController($mockGateway, $mockCommerce, $webhookLogger);
$request = new \Illuminate\Http\Request;
$request = new Request;
$response = $controller->handle($request);
expect($response->getStatusCode())->toBe(200);
@ -629,7 +633,7 @@ describe('BTCPayWebhookController', function () {
$controller = new BTCPayWebhookController($mockGateway, $mockCommerce, $webhookLogger);
$request = new \Illuminate\Http\Request;
$request = new Request;
$response = $controller->handle($request);
expect($response->getStatusCode())->toBe(200);
@ -656,7 +660,7 @@ describe('BTCPayWebhookController', function () {
$controller = new BTCPayWebhookController($mockGateway, $mockCommerce, $webhookLogger);
$request = new \Illuminate\Http\Request;
$request = new Request;
$response = $controller->handle($request);
expect($response->getStatusCode())->toBe(200);
@ -683,7 +687,7 @@ describe('BTCPayWebhookController', function () {
$controller = new BTCPayWebhookController($mockGateway, $mockCommerce, $webhookLogger);
$request = new \Illuminate\Http\Request;
$request = new Request;
$response = $controller->handle($request);
expect($response->getStatusCode())->toBe(200)
@ -840,7 +844,7 @@ describe('WebhookLogger service', function () {
it('extracts relevant headers', function () {
$logger = new WebhookLogger;
$request = new \Illuminate\Http\Request;
$request = new Request;
$request->headers->set('Stripe-Signature', 't=123,v1=secret_signature_here');
$request->headers->set('Content-Type', 'application/json');
$request->headers->set('User-Agent', 'Stripe/1.0');
@ -1235,7 +1239,7 @@ describe('BTCPayWebhookController payload validation integration', function () {
$webhookLogger = new WebhookLogger;
$controller = new BTCPayWebhookController($mockGateway, $mockCommerce, $webhookLogger);
$request = new \Illuminate\Http\Request;
$request = new Request;
$request->headers->set('BTCPay-Sig', 'valid_signature');
$response = $controller->handle($request);
@ -1262,7 +1266,7 @@ describe('BTCPayWebhookController payload validation integration', function () {
$controller = new BTCPayWebhookController($mockGateway, $mockCommerce, $webhookLogger);
$request = new \Illuminate\Http\Request;
$request = new Request;
$response = $controller->handle($request);
expect($response->getStatusCode())->toBe(400)
@ -1282,7 +1286,7 @@ describe('BTCPayWebhookController payload validation integration', function () {
$controller = new BTCPayWebhookController($mockGateway, $mockCommerce, $webhookLogger);
$request = new \Illuminate\Http\Request;
$request = new Request;
$response = $controller->handle($request);
expect($response->getStatusCode())->toBe(400)
@ -1313,7 +1317,7 @@ describe('BTCPayWebhookController payload validation integration', function () {
$webhookLogger = new WebhookLogger;
$controller = new BTCPayWebhookController($mockGateway, $mockCommerce, $webhookLogger);
$request = new \Illuminate\Http\Request;
$request = new Request;
$response = $controller->handle($request);
expect($response->getStatusCode())->toBe(200);
@ -1388,7 +1392,7 @@ describe('Webhook Idempotency (Replay Attack Protection)', function () {
$webhookLogger = new WebhookLogger;
$controller = new BTCPayWebhookController($mockGateway, $mockCommerce, $webhookLogger);
$request = new \Illuminate\Http\Request;
$request = new Request;
$response = $controller->handle($request);
expect($response->getStatusCode())->toBe(200)
@ -1423,7 +1427,7 @@ describe('Webhook Idempotency (Replay Attack Protection)', function () {
$controller = new BTCPayWebhookController($mockGateway, $mockCommerce, $webhookLogger);
// First request - should process
$request1 = new \Illuminate\Http\Request;
$request1 = new Request;
$response1 = $controller->handle($request1);
expect($response1->getStatusCode())->toBe(200);
@ -1437,7 +1441,7 @@ describe('Webhook Idempotency (Replay Attack Protection)', function () {
// Second request with same event ID - should be rejected as duplicate
$webhookLogger2 = new WebhookLogger;
$controller2 = new BTCPayWebhookController($mockGateway, $mockCommerce, $webhookLogger2);
$request2 = new \Illuminate\Http\Request;
$request2 = new Request;
$response2 = $controller2->handle($request2);
expect($response2->getStatusCode())->toBe(200)
@ -1517,7 +1521,7 @@ describe('Webhook Idempotency (Replay Attack Protection)', function () {
$webhookLogger
);
$request = new \Illuminate\Http\Request;
$request = new Request;
$response = $controller->handle($request);
expect($response->getStatusCode())->toBe(200)
@ -1574,7 +1578,7 @@ describe('BTCPay Payment Amount Verification', function () {
$webhookLogger = new WebhookLogger;
$controller = new BTCPayWebhookController($mockGateway, $mockCommerce, $webhookLogger);
$request = new \Illuminate\Http\Request;
$request = new Request;
$response = $controller->handle($request);
expect($response->getStatusCode())->toBe(200);
@ -1610,7 +1614,7 @@ describe('BTCPay Payment Amount Verification', function () {
$webhookLogger = new WebhookLogger;
$controller = new BTCPayWebhookController($mockGateway, $mockCommerce, $webhookLogger);
$request = new \Illuminate\Http\Request;
$request = new Request;
$response = $controller->handle($request);
expect($response->getStatusCode())->toBe(200)
@ -1653,7 +1657,7 @@ describe('BTCPay Payment Amount Verification', function () {
$webhookLogger = new WebhookLogger;
$controller = new BTCPayWebhookController($mockGateway, $mockCommerce, $webhookLogger);
$request = new \Illuminate\Http\Request;
$request = new Request;
$response = $controller->handle($request);
expect($response->getStatusCode())->toBe(200);
@ -1690,7 +1694,7 @@ describe('BTCPay Payment Amount Verification', function () {
$webhookLogger = new WebhookLogger;
$controller = new BTCPayWebhookController($mockGateway, $mockCommerce, $webhookLogger);
$request = new \Illuminate\Http\Request;
$request = new Request;
$response = $controller->handle($request);
expect($response->getStatusCode())->toBe(200)
@ -1727,7 +1731,7 @@ describe('BTCPay Payment Amount Verification', function () {
$webhookLogger = new WebhookLogger;
$controller = new BTCPayWebhookController($mockGateway, $mockCommerce, $webhookLogger);
$request = new \Illuminate\Http\Request;
$request = new Request;
$response = $controller->handle($request);
expect($response->getStatusCode())->toBe(200);

View file

@ -4,6 +4,7 @@ declare(strict_types=1);
namespace Tests;
use Core\Mod\Commerce\Boot;
use Orchestra\Testbench\TestCase as BaseTestCase;
abstract class TestCase extends BaseTestCase
@ -11,7 +12,7 @@ abstract class TestCase extends BaseTestCase
protected function getPackageProviders($app): array
{
return [
\Core\Mod\Commerce\Boot::class,
Boot::class,
];
}
}

View file

@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
/**
* UseCase: Commerce Admin CRUD (Basic Flow)
*