feat: subscriptions
CI / 🧪 Tests Laravel (push) Successful in 2m48s
CI / 🐳 Build & Push Image (push) Successful in 3m0s

This commit is contained in:
2026-08-07 11:53:46 +02:00
parent 99cb0e63e6
commit 2ece4b6e3e
51 changed files with 1489 additions and 254 deletions
+3
View File
@@ -2,6 +2,7 @@
namespace App\Models;
use App\Enums\BillingProductPurpose;
use App\Enums\CreditProductType;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Concerns\HasUlids;
@@ -18,6 +19,7 @@ class CreditProduct extends Model
'name',
'description',
'type',
'purpose',
'credits',
'amount',
'currency',
@@ -51,6 +53,7 @@ class CreditProduct extends Model
{
return [
'type' => CreditProductType::class,
'purpose' => BillingProductPurpose::class,
'credits' => 'integer',
'amount' => 'integer',
'is_active' => 'boolean',
@@ -0,0 +1,73 @@
<?php
namespace App\Models;
use App\Enums\IdentityVerificationStatus;
use Illuminate\Database\Eloquent\Concerns\HasUlids;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Support\Facades\Storage;
class IdentityVerificationRequest extends Model
{
/** @use HasFactory<\Database\Factories\IdentityVerificationRequestFactory> */
use HasFactory, HasUlids;
protected $fillable = [
'user_id',
'status',
'document_disk',
'document_path',
'document_mime_type',
'document_size',
'submitted_at',
'reviewed_at',
'reviewed_by',
'rejection_reason',
'document_deleted_at',
];
protected $hidden = [
'document_disk',
'document_path',
];
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
public function reviewer(): BelongsTo
{
return $this->belongsTo(User::class, 'reviewed_by');
}
public function hasDocument(): bool
{
return filled($this->document_path) && $this->document_deleted_at === null;
}
public function temporaryDocumentUrl(): ?string
{
if (! $this->hasDocument()) {
return null;
}
return Storage::disk($this->document_disk)->temporaryUrl(
$this->document_path,
now()->addMinutes((int) config('billing.identity_verification.temporary_url_minutes', 5)),
);
}
protected function casts(): array
{
return [
'status' => IdentityVerificationStatus::class,
'submitted_at' => 'datetime',
'reviewed_at' => 'datetime',
'document_deleted_at' => 'datetime',
'document_size' => 'integer',
];
}
}
+27
View File
@@ -47,6 +47,8 @@ class User extends Authenticatable implements FilamentUser, HasAvatar, HasLocale
'height',
'bio',
'account_verified_at',
'certification_purchased_at',
'trial_ends_at',
'daily_calorie_goal',
'daily_protein_goal',
'daily_carbs_goal',
@@ -82,6 +84,7 @@ class User extends Authenticatable implements FilamentUser, HasAvatar, HasLocale
return [
'email_verified_at' => 'datetime',
'account_verified_at' => 'datetime',
'certification_purchased_at' => 'datetime',
'password' => 'hashed',
'daily_calorie_goal' => 'integer',
'daily_protein_goal' => 'float',
@@ -129,6 +132,11 @@ class User extends Authenticatable implements FilamentUser, HasAvatar, HasLocale
return $this->hasMany(PaymentTransaction::class);
}
public function identityVerificationRequest(): HasOne
{
return $this->hasOne(IdentityVerificationRequest::class);
}
public function workouts(): HasMany
{
return $this->hasMany(WorkoutSessions::class);
@@ -188,6 +196,25 @@ class User extends Authenticatable implements FilamentUser, HasAvatar, HasLocale
return $this->suspended_at !== null;
}
public function canAnalyzeMeals(): bool
{
return $this->subscribed('default') || $this->onGenericTrial();
}
public function analysisAccessLevel(): string
{
if ($this->subscribed('default')) {
return 'subscribed';
}
return $this->onGenericTrial() ? 'trial' : 'free';
}
public function hasPurchasedCertification(): bool
{
return $this->certification_purchased_at !== null;
}
public function routeNotificationForExpoPush(): array
{
return $this->deviceTokens()