This commit is contained in:
saeid 2024-01-25 04:31:26 +03:30
parent f4c6bcade1
commit 588d93c4a2
28 changed files with 537 additions and 116 deletions

View File

@ -4,7 +4,8 @@
"type": "library",
"license": "MIT",
"require": {
"php": "^8.2"
"php": "^8.2",
"google/apiclient": "^2.15.0"
},
"autoload": {
"psr-4": {
@ -23,7 +24,11 @@
"aliases": {
"Subscription": "IICN\\Subscription\\Subscription"
}
}
},
"google/apiclient-services": [
"Drive",
"YouTube"
]
},
"require-dev": {
"phpunit/phpunit": "^10.5.5",
@ -33,5 +38,8 @@
"psr-4": {
"IICN\\Subscription\\Tests\\": "tests/"
}
},
"scripts": {
"pre-autoload-dump": "Google\\Task\\Composer::cleanup"
}
}

View File

@ -1,7 +1,25 @@
<?php
return [
'user_model' => \App\Models\User::class,
'coupon_conditions' => [
],
'guard' => null,
'enumerations' => [
'subscription_ability_types' => ['istikhara', 'thematicـquran'],
'subscription_types' => ['premium', 'freemium', 'packages'],
],
'google' => [
'application_name' => '',
'app_key' => '',
'package_name' => '',
],
'response_class' => null
];

View File

@ -17,8 +17,8 @@ return new class extends Migration
$table->integer('duration_day')->default(-1);
$table->unsignedInteger('price')->default(0);
$table->unsignedInteger('discount_percent')->default(0);
$table->string('sku_code');
$table->string('type')->default('subscription');
$table->string('sku_code')->unique();
$table->string('type');
$table->integer('count')->default(0)->comment("just for show to Front");
$table->json('description')->nullable();
$table->timestamps();

View File

@ -17,7 +17,7 @@ return new class extends Migration
$table->string('name');
$table->string('type');
$table->unsignedInteger('count')->default(999999);
$table->unsignedInteger('subscription_id')->nullable();
$table->unsignedInteger('subscription_id')->index();
$table->string('description')->nullable();
$table->timestamps();
});

View File

@ -13,9 +13,10 @@ return new class extends Migration
{
Schema::create('subscription_logs', function (Blueprint $table) {
$table->id();
$table->unsignedBigInteger('subscription_ability_id');
$table->unsignedBigInteger('user_id');
$table->enum('type', ['used', 'rollback', 'new']);
$table->unsignedBigInteger('subscription_user_id')->index();
$table->unsignedBigInteger('user_id')->index();
$table->json('new');
$table->json('old');
$table->timestamps();
});
}

View File

@ -0,0 +1,34 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('subscription_coupons', function (Blueprint $table) {
$table->id();
$table->string('code')->index();
$table->unsignedBigInteger('user_id')->nullable()->index();
$table->unsignedBigInteger('subscription_id')->index();
$table->unsignedBigInteger('count')->default(0);
$table->timestamp('expiry_at');
$table->integer('duration_day')->default(-1);
$table->timestamps();
$table->softDeletes();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('subscription_coupons');
}
};

View File

@ -0,0 +1,36 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('subscription_transactions', function (Blueprint $table) {
$table->id();
$table->unsignedBigInteger('user_id')->index();
$table->unsignedBigInteger('subscription_user_id')->nullable()->index();
$table->unsignedBigInteger('price')->default(0);
$table->unsignedBigInteger('subscription_coupon_id')->nullable()->index();
$table->enum('status', [
\IICN\Subscription\Constants\Status::INIT,
\IICN\Subscription\Constants\Status::SUCCESS,
\IICN\Subscription\Constants\Status::FAILED,
])->default(\IICN\Subscription\Constants\Status::INIT);
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('subscription_transactions');
}
};

View File

@ -1,81 +0,0 @@
<?php
namespace IICN\Subscription\Concerns;
trait AllowsCoupons
{
/**
* The coupon ID being applied.
*
* @var string|null
*/
public $couponId;
/**
* The promotion code ID being applied.
*
* @var string|null
*/
public $promotionCodeId;
/**
* Determines if user redeemable promotion codes are available in Stripe Checkout.
*
* @var bool
*/
public $allowPromotionCodes = false;
/**
* The coupon ID to be applied.
*
* @param string $couponId
* @return $this
*/
public function withCoupon($couponId)
{
$this->couponId = $couponId;
return $this;
}
/**
* The promotion code ID to apply.
*
* @param string $promotionCodeId
* @return $this
*/
public function withPromotionCode($promotionCodeId)
{
$this->promotionCodeId = $promotionCodeId;
return $this;
}
/**
* Enables user redeemable promotion codes for a Stripe Checkout session.
*
* @return $this
*/
public function allowPromotionCodes()
{
$this->allowPromotionCodes = true;
return $this;
}
/**
* Return the discounts for a Stripe Checkout session.
*
* @return array[]|null
*/
protected function checkoutDiscounts()
{
if ($this->couponId) {
return [['coupon' => $this->couponId]];
}
if ($this->promotionCodeId) {
return [['promotion_code' => $this->promotionCodeId]];
}
}
}

View File

@ -30,6 +30,13 @@ trait ManagesSubscriptions
return $subscription->getActiveWithCount($type);
}
public function getSubscriptionTypes(): array
{
$subscription = new Subscription($this);
return $subscription->getSubscriptionTypes();
}
public function activeSubscriptionsWithType(string $type): BelongsToMany
{
return $this->activeSubscriptions()->whereHas('subscriptionAbilities', function ($query) use ($type) {

10
src/Constants/Status.php Normal file
View File

@ -0,0 +1,10 @@
<?php
namespace IICN\Subscription\Constants;
class Status
{
const SUCCESS = 'success';
const FAILED = 'failed';
const INIT = 'int';
}

View File

@ -0,0 +1,13 @@
<?php
namespace IICN\Subscription;
use IICN\Subscription\Models\SubscriptionCoupon;
interface CouponConditionInterface
{
public function handle(SubscriptionCoupon $subscriptionCoupon): bool;
public function failedMessage(): string;
}

View File

@ -0,0 +1,18 @@
<?php
namespace IICN\Subscription\Http\Controllers\Subscription;
use IICN\Subscription\Http\Controllers\Controller;
use IICN\Subscription\Http\Resources\SubscriptionResources;
use IICN\Subscription\Models\Subscription;
use Illuminate\Http\Resources\Json\AnonymousResourceCollection;
class Index extends Controller
{
public function __invoke(): AnonymousResourceCollection
{
$subscriptions = Subscription::query()->get();
return SubscriptionResources::collection($subscriptions);
}
}

View File

@ -0,0 +1,18 @@
<?php
namespace IICN\Subscription\Http\Controllers\Subscription;
use IICN\Subscription\Http\Controllers\Controller;
use IICN\Subscription\Http\Resources\SubscriptionResources;
use IICN\Subscription\Models\Subscription;
use Illuminate\Http\Resources\Json\AnonymousResourceCollection;
class IndexByType extends Controller
{
public function __invoke(string $type): AnonymousResourceCollection
{
$subscriptions = Subscription::query()->with('subscriptionAbilities')->where('type', $type)->get();
return SubscriptionResources::collection($subscriptions);
}
}

View File

@ -0,0 +1,57 @@
<?php
namespace IICN\Subscription\Http\Controllers\Subscription;
use IICN\Subscription\Constants\Status;
use IICN\Subscription\Http\Controllers\Controller;
use IICN\Subscription\Models\Subscription;
use IICN\Subscription\Models\SubscriptionTransaction;
use IICN\Subscription\Models\SubscriptionUser;
use IICN\Subscription\Services\Playstore\Playstore;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
class VerifyPurchase extends Controller
{
public function __invoke(Subscription $subscription, Request $request)
{
$playstore = new Playstore();
$result = $playstore->verifyPurchase($subscription->sku_code, $request->purechaseToken);
if (!$result) {
SubscriptionTransaction::query()->create([
'user_id' => \IICN\Subscription\Subscription::getSubscriptionableId(),
'status' => Status::FAILED,
]);
return false;
}
DB::beginTransaction();
$result = \IICN\Subscription\Subscription::create($subscription->id);
if (!$result) {
DB::rollBack();
return false;
}
$result = SubscriptionTransaction::query()->create([
'user_id' => \IICN\Subscription\Subscription::getSubscriptionableId(),
'subscription_user_id' => SubscriptionUser::query()->where([
'subscription_id' => $subscription->id,
'user_id' => \IICN\Subscription\Subscription::getSubscriptionableId(),
])->latest('id')->first()?->id,
'status' => Status::SUCCESS,
]);
if (!$result) {
DB::rollBack();
return false;
}
DB::commit();
return true;
}
}

View File

@ -0,0 +1,53 @@
<?php
namespace IICN\Subscription\Http\Controllers\SubscriptionCoupon;
use IICN\Subscription\Constants\Status;
use IICN\Subscription\Http\Controllers\Controller;
use IICN\Subscription\Http\Requests\StoreWithSubscriptionCouponRequest;
use IICN\Subscription\Models\SubscriptionCoupon;
use IICN\Subscription\Models\SubscriptionUser;
use IICN\Subscription\Subscription;
use Illuminate\Support\Facades\DB;
class StoreWithSubscriptionCoupon extends Controller
{
public function __invoke(StoreWithSubscriptionCouponRequest $request)
{
$subscriptionCoupon = SubscriptionCoupon::query()->find($request->validated('subscription_coupon_id'));
DB::beginTransaction();
$result = Subscription::create($subscriptionCoupon->subscription_id, $subscriptionCoupon->duration_day);
if (!$result) {
DB::rollBack();
return false;
}
$result = $subscriptionCoupon->subscriptionTransactions()->create([
'user_id' => Subscription::getSubscriptionableId(),
'subscription_user_id' => SubscriptionUser::query()->where([
'subscription_id' => $subscriptionCoupon->subscription_id,
'user_id' => Subscription::getSubscriptionableId(),
])->latest('id')->first()?->id,
'status' => Status::SUCCESS,
]);
if (!$result) {
DB::rollBack();
return false;
}
$result = $subscriptionCoupon->update(['count' => $subscriptionCoupon->count - 1]);
if (!$result) {
DB::rollBack();
return false;
}
DB::commit();
return true;
}
}

View File

@ -11,7 +11,8 @@ class Test extends Controller
public function __invoke()
{
Auth::loginUsingId(1);
return Subscription::canUse('Istikhara');
// return Subscription::create(1);
return Subscription::getSubscriptionTypes();
return $user->useSubscription("Istikhara");
$user->newSubscription(1);
}

View File

@ -21,7 +21,7 @@ class ValidateSubscription
if (Subscription::canUse($type)) {
return $next($request);
} else {
abort(403);
abort(403, 'You do not have access to this Ability');
}
}
}

View File

@ -0,0 +1,66 @@
<?php
namespace IICN\Subscription\Http\Requests;
use Carbon\Carbon;
use IICN\Subscription\CouponConditionInterface;
use IICN\Subscription\Models\SubscriptionCoupon;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\ValidationException;
class StoreWithSubscriptionCouponRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
return \Illuminate\Support\Facades\Auth::guard(config('subscription.guard'))->check();
}
/**
* Get the validation rules that apply to the request.
*
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
*/
public function rules(): array
{
return [
'code' => 'required|string|max:100',
'subscription_coupon_id' => 'required',
];
}
/**
* @throws ValidationException
*/
public function prepareForValidation()
{
if (!isset($this->code)) {
return true;
}
$subscriptionCoupon = SubscriptionCoupon::query()
->where('code', $this->code)
->where('expiry_at', '>=', Carbon::now())
->where('count', '>', 0)
->first();
if ($subscriptionCoupon) {
$this->merge(['subscription_coupon_id' => $subscriptionCoupon->id]);
} else {
return throw ValidationException::withMessages(['code' => 'code not found!']);
}
$conditions = config('subscription.coupon_conditions');
foreach ($conditions as $condition) {
if ($condition instanceof CouponConditionInterface) {
$classCondition = new $condition();
if (!$classCondition->handle($subscriptionCoupon)) {
return throw ValidationException::withMessages(['message' => $classCondition->failedMessage()]);
}
}
}
}
}

View File

@ -0,0 +1,22 @@
<?php
namespace IICN\Subscription\Http\Resources;
use Illuminate\Http\Resources\Json\JsonResource;
class SubscriptionAbilityResources extends JsonResource
{
public function toArray($request)
{
return [
'id' => $this->id,
'slug' => $this->slug,
'name' => $this->name,
'type' => $this->type,
'count' => $this->count,
'subscription_id' => $this->subscription_id,
'description' => $this->description,
'subscription' => SubscriptionResources::make($this->whenLoaded('subscription')),
];
}
}

View File

@ -0,0 +1,24 @@
<?php
namespace IICN\Subscription\Http\Resources;
use Illuminate\Http\Resources\Json\JsonResource;
class SubscriptionResources extends JsonResource
{
public function toArray($request)
{
return [
'id' => $this->id,
'title' => $this->title,
'duration_day' => $this->duration_day,
'price' => $this->price,
'discount_percent' => $this->discount_percent,
'sku_code' => $this->sku_code,
'type' => $this->type,
'count' => $this->count,
'description' => $this->description,
'subscriptionAbilities' => SubscriptionAbilityResources::collection($this->whenLoaded('subscriptionAbilities')),
];
}
}

View File

@ -2,9 +2,7 @@
namespace IICN\Subscription\Models;
use Carbon\Carbon;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\SoftDeletes;
@ -28,23 +26,8 @@ class Subscription extends Model
'description',
];
// public function users(): BelongsToMany
// {
// return $this->belongsToMany(config('subscription.user_model'), 'subscription_user')->with(['created_at', 'expiry_at', 'remaining_number']);
// }
public function subscriptionAbilities(): HasMany
{
return $this->hasMany(SubscriptionAbility::class);
}
public function activeUsers(): BelongsToMany
{
return $this->belongsToMany(config('subscription.user_model'), 'subscription_user')
->where(function ($query) {
$query->wherePivot('expiry_at', '>', Carbon::now())->orWherePivotNull('expiry_at');
})
->wherePivot('remaining_number', '>', 0)
->with(['created_at', 'expiry_at', 'remaining_number']);
}
}

View File

@ -17,7 +17,6 @@ class SubscriptionAbility extends Model
'name',
'type',
'count',
'condition_class',
'subscription_id',
'description',
];

View File

@ -0,0 +1,30 @@
<?php
namespace IICN\Subscription\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\SoftDeletes;
class SubscriptionCoupon extends Model
{
use SoftDeletes;
/**
* The attributes that are mass assignable.
*
* @var array<int, string>
*/
protected $fillable = ['subscription_id', 'user_id', 'count', 'expiry_at', 'duration_day', 'code'];
public function subscription(): BelongsTo
{
return $this->belongsTo(Subscription::class);
}
public function subscriptionTransactions(): HasMany
{
return $this->hasMany(SubscriptionTransaction::class);
}
}

View File

@ -0,0 +1,24 @@
<?php
namespace IICN\Subscription\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\SoftDeletes;
class SubscriptionLog extends Model
{
use SoftDeletes;
/**
* The attributes that are mass assignable.
*
* @var array<int, string>
*/
protected $fillable = ['subscription_user_id', 'user_id', 'new', 'old'];
public function subscriptionUser(): BelongsTo
{
return $this->belongsTo(SubscriptionUser::class);
}
}

View File

@ -0,0 +1,35 @@
<?php
namespace IICN\Subscription\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\SoftDeletes;
class SubscriptionTransaction extends Model
{
use SoftDeletes;
/**
* The attributes that are mass assignable.
*
* @var array<int, string>
*/
protected $fillable = [
'user_id',
'price',
'subscription_user_id',
'subscription_coupon_id',
'status'
];
public function subscriptionUser(): BelongsTo
{
return $this->belongsTo(SubscriptionUser::class);
}
public function subscriptionCoupon(): BelongsTo
{
return $this->belongsTo(SubscriptionCoupon::class);
}
}

View File

@ -0,0 +1,32 @@
<?php
namespace IICN\Subscription\Services\Playstore;
use Google\Client;
use Google\Service\AndroidPublisher;
class Playstore
{
public $service;
public function __construct()
{
$client = new Client();
$client->setApplicationName(config('subscription.google.application_name'));
$client->setDeveloperKey(config('subscription.google.app_key'));
// $client->setAuthConfig('path/to/credentials.json');
// $client->setScopes(['https://www.googleapis.com/auth/androidpublisher']);
$this->service = new AndroidPublisher($client);
}
public function verifyPurchase(string $productId, string $purchaseToken)
{
$packageName = config('subscription.google.package_name');
$result = $this->service->purchases_products->get($packageName, $productId, $purchaseToken);
return $result->getPurchaseState() == 0;
}
}

View File

@ -12,19 +12,20 @@ class Subscription
{
}
public function create(int $subscription_id): bool
public function create(int $subscription_id, ?int $durationDay = null): bool
{
$subscription = \IICN\Subscription\Models\Subscription::query()->find($subscription_id);
if (isset($subscription->id)) {
$expiryAt = Carbon::now()->addDays($subscription->duration_day);
$data = [];
$expiryAt = $durationDay ? Carbon::now()->addDays($durationDay) : Carbon::now()->addDays($subscription->duration_day);
$remainingNumber = [];
foreach ($subscription->subscriptionAbilities as $ability) {
$data[$ability->type] = $ability->count;
$remainingNumber[$ability->type] = $ability->count;
}
$this->model->subscriptions()->attach($subscription_id, ['expiry_at' => $expiryAt, 'remaining_number' => $data]);
dd($this->model->subscriptions()->attach($subscription_id, ['expiry_at' => $expiryAt, 'remaining_number' => $remainingNumber]));
return true;
}
@ -71,4 +72,14 @@ class Subscription
{
return isset($this->getActiveWithCount($type)[0]);
}
public function getSubscriptionTypes(): array
{
return array_unique($this->model->activeSubscriptions->pluck('type')->toArray());
}
public function getSubscriptionableId(): int|null|string
{
return $this->model->id;
}
}

View File

@ -4,9 +4,11 @@ namespace IICN\Subscription;
use Illuminate\Support\Facades\Facade;
/**
* @method static \IICN\Subscription\Services\Subscription create(int $subscription_id)
* @method static \IICN\Subscription\Services\Subscription create(int $subscription_id, ?int $durationDay = null)
* @method static \IICN\Subscription\Services\Subscription used(string $type)
* @method static \IICN\Subscription\Services\Subscription canUse(string $type)
* @method static \IICN\Subscription\Services\Subscription getSubscriptionTypes()
* @method static \IICN\Subscription\Services\Subscription getSubscriptionableId()
*
* @see \IICN\Subscription\Services\Subscription
*/