diff --git a/composer.json b/composer.json index fd5f9fa..3c87c7d 100644 --- a/composer.json +++ b/composer.json @@ -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" } } diff --git a/config/subscription.php b/config/subscription.php index 63ace6d..3c6c082 100644 --- a/config/subscription.php +++ b/config/subscription.php @@ -1,7 +1,25 @@ \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 ]; diff --git a/database/migrations/2024_01_14_153551_create_subscriptions_table.php b/database/migrations/2024_01_14_153551_create_subscriptions_table.php index 1f11f8b..75695d7 100644 --- a/database/migrations/2024_01_14_153551_create_subscriptions_table.php +++ b/database/migrations/2024_01_14_153551_create_subscriptions_table.php @@ -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(); diff --git a/database/migrations/2024_01_14_164441_create_subscription_abilities_table.php b/database/migrations/2024_01_14_164441_create_subscription_abilities_table.php index c3f39ff..09c5cd4 100644 --- a/database/migrations/2024_01_14_164441_create_subscription_abilities_table.php +++ b/database/migrations/2024_01_14_164441_create_subscription_abilities_table.php @@ -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(); }); diff --git a/database/migrations/2024_01_14_164441_create_subscription_logs_table.php b/database/migrations/2024_01_14_164441_create_subscription_logs_table.php index f621212..56ae28a 100644 --- a/database/migrations/2024_01_14_164441_create_subscription_logs_table.php +++ b/database/migrations/2024_01_14_164441_create_subscription_logs_table.php @@ -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(); }); } diff --git a/database/migrations/2024_01_21_170955_create_subscription_coupons_table.php b/database/migrations/2024_01_21_170955_create_subscription_coupons_table.php new file mode 100644 index 0000000..118e254 --- /dev/null +++ b/database/migrations/2024_01_21_170955_create_subscription_coupons_table.php @@ -0,0 +1,34 @@ +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'); + } +}; diff --git a/database/migrations/2024_01_24_140834_create_subscription_transactions_table.php b/database/migrations/2024_01_24_140834_create_subscription_transactions_table.php new file mode 100644 index 0000000..fe9b3e4 --- /dev/null +++ b/database/migrations/2024_01_24_140834_create_subscription_transactions_table.php @@ -0,0 +1,36 @@ +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'); + } +}; diff --git a/src/Concerns/AllowsCoupons.php b/src/Concerns/AllowsCoupons.php deleted file mode 100644 index 0b5285f..0000000 --- a/src/Concerns/AllowsCoupons.php +++ /dev/null @@ -1,81 +0,0 @@ -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]]; - } - } -} diff --git a/src/Concerns/ManagesSubscriptions.php b/src/Concerns/ManagesSubscriptions.php index 30143e7..9b51dad 100644 --- a/src/Concerns/ManagesSubscriptions.php +++ b/src/Concerns/ManagesSubscriptions.php @@ -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) { diff --git a/src/Constants/Status.php b/src/Constants/Status.php new file mode 100644 index 0000000..4dc2969 --- /dev/null +++ b/src/Constants/Status.php @@ -0,0 +1,10 @@ +get(); + + return SubscriptionResources::collection($subscriptions); + } +} diff --git a/src/Http/Controllers/Subscription/IndexByType.php b/src/Http/Controllers/Subscription/IndexByType.php new file mode 100644 index 0000000..7299d68 --- /dev/null +++ b/src/Http/Controllers/Subscription/IndexByType.php @@ -0,0 +1,18 @@ +with('subscriptionAbilities')->where('type', $type)->get(); + + return SubscriptionResources::collection($subscriptions); + } +} diff --git a/src/Http/Controllers/Subscription/VerifyPurchase.php b/src/Http/Controllers/Subscription/VerifyPurchase.php new file mode 100644 index 0000000..b528e59 --- /dev/null +++ b/src/Http/Controllers/Subscription/VerifyPurchase.php @@ -0,0 +1,57 @@ +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; + } +} diff --git a/src/Http/Controllers/SubscriptionCoupon/StoreWithSubscriptionCoupon.php b/src/Http/Controllers/SubscriptionCoupon/StoreWithSubscriptionCoupon.php new file mode 100644 index 0000000..8b6c5bc --- /dev/null +++ b/src/Http/Controllers/SubscriptionCoupon/StoreWithSubscriptionCoupon.php @@ -0,0 +1,53 @@ +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; + } +} diff --git a/src/Http/Controllers/Test/Test.php b/src/Http/Controllers/Test/Test.php index 32d70a2..d44cd44 100644 --- a/src/Http/Controllers/Test/Test.php +++ b/src/Http/Controllers/Test/Test.php @@ -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); } diff --git a/src/Http/Middleware/ValidateSubscription.php b/src/Http/Middleware/ValidateSubscription.php index 32ef904..40ebbc9 100644 --- a/src/Http/Middleware/ValidateSubscription.php +++ b/src/Http/Middleware/ValidateSubscription.php @@ -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'); } } } diff --git a/src/Http/Requests/StoreWithSubscriptionCouponRequest.php b/src/Http/Requests/StoreWithSubscriptionCouponRequest.php new file mode 100644 index 0000000..17933ec --- /dev/null +++ b/src/Http/Requests/StoreWithSubscriptionCouponRequest.php @@ -0,0 +1,66 @@ +check(); + } + + /** + * Get the validation rules that apply to the request. + * + * @return array|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()]); + } + } + } + } +} diff --git a/src/Http/Resources/SubscriptionAbilityResources.php b/src/Http/Resources/SubscriptionAbilityResources.php new file mode 100644 index 0000000..17c8bfc --- /dev/null +++ b/src/Http/Resources/SubscriptionAbilityResources.php @@ -0,0 +1,22 @@ + $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')), + ]; + } +} diff --git a/src/Http/Resources/SubscriptionResources.php b/src/Http/Resources/SubscriptionResources.php new file mode 100644 index 0000000..5995a33 --- /dev/null +++ b/src/Http/Resources/SubscriptionResources.php @@ -0,0 +1,24 @@ + $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')), + ]; + } +} diff --git a/src/Models/Subscription.php b/src/Models/Subscription.php index f1d6a3c..3c7fbd4 100644 --- a/src/Models/Subscription.php +++ b/src/Models/Subscription.php @@ -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']); - } } diff --git a/src/Models/SubscriptionAbility.php b/src/Models/SubscriptionAbility.php index e593c0d..5898874 100644 --- a/src/Models/SubscriptionAbility.php +++ b/src/Models/SubscriptionAbility.php @@ -17,7 +17,6 @@ class SubscriptionAbility extends Model 'name', 'type', 'count', - 'condition_class', 'subscription_id', 'description', ]; diff --git a/src/Models/SubscriptionCoupon.php b/src/Models/SubscriptionCoupon.php new file mode 100644 index 0000000..e11df1a --- /dev/null +++ b/src/Models/SubscriptionCoupon.php @@ -0,0 +1,30 @@ + + */ + 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); + } +} diff --git a/src/Models/SubscriptionLog.php b/src/Models/SubscriptionLog.php new file mode 100644 index 0000000..49dcf9a --- /dev/null +++ b/src/Models/SubscriptionLog.php @@ -0,0 +1,24 @@ + + */ + protected $fillable = ['subscription_user_id', 'user_id', 'new', 'old']; + + public function subscriptionUser(): BelongsTo + { + return $this->belongsTo(SubscriptionUser::class); + } +} diff --git a/src/Models/SubscriptionTransaction.php b/src/Models/SubscriptionTransaction.php new file mode 100644 index 0000000..0da3567 --- /dev/null +++ b/src/Models/SubscriptionTransaction.php @@ -0,0 +1,35 @@ + + */ + 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); + } +} diff --git a/src/Services/Playstore/Playstore.php b/src/Services/Playstore/Playstore.php new file mode 100644 index 0000000..c8a15c5 --- /dev/null +++ b/src/Services/Playstore/Playstore.php @@ -0,0 +1,32 @@ +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; + } +} diff --git a/src/Services/Subscription.php b/src/Services/Subscription.php index e68ad7a..d22dc38 100644 --- a/src/Services/Subscription.php +++ b/src/Services/Subscription.php @@ -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; + } } diff --git a/src/Subscription.php b/src/Subscription.php index 505826d..4819902 100644 --- a/src/Subscription.php +++ b/src/Subscription.php @@ -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 */