diff --git a/app/Http/Controllers/api/HomeController.php b/app/Http/Controllers/api/HomeController.php index a35b87a..2a8446a 100644 --- a/app/Http/Controllers/api/HomeController.php +++ b/app/Http/Controllers/api/HomeController.php @@ -3,40 +3,26 @@ namespace App\Http\Controllers\api; use App\Http\Controllers\Controller; -// 1. IMPORT THE JOB WE CREATED -use App\Jobs\CheckBazaarSubscription; - -// These are your existing imports use App\Models\Law; use App\Models\Notification; use App\Models\RecentArt; use App\Models\UserSubscriber; +use App\Services\AppMarketPurchaseVerifier; use App\Traits\BaseApiResponse; -use Carbon\Carbon; -use Illuminate\Support\Facades\Log; class HomeController extends Controller { use BaseApiResponse; + public function __construct(private AppMarketPurchaseVerifier $marketVerifier) + { + } + public function index() { $user = auth()->user(); - // 2. DISPATCH THE JOB AT THE VERY BEGINNING - // =================================================================== - // Find the user's latest subscription that has a purchase token - $latestSubscription = $user->userSubscribers()->whereNotNull('purchase_token')->latest()->first(); - - // If such a subscription exists, create a new job and hand it to the queue - // if ($latestSubscription) { - // CheckBazaarSubscription::dispatch($latestSubscription); - // } - // =================================================================== - // Your API now continues immediately without waiting for the check to finish. - - - // --- ALL THE REST OF YOUR CODE REMAINS EXACTLY THE SAME --- + $this->refreshMarketSubscription($user); $recent = RecentArt::query()->where('user_id', $user->id)->get()->map(function ($q) { return [ @@ -81,8 +67,6 @@ class HomeController extends Controller ]; }); - $current_plan = null; - $freeSubscription = $user->userSubscribers() ->whereHas('subscribe', function ($query) { $query->where('is_free', true); @@ -90,11 +74,9 @@ class HomeController extends Controller ->where('expired_at', '>=', now()) ->first(); - $expiredAt = null; $current_plan = null; if ($freeSubscription) { - $expiredAt = $freeSubscription->expired_at; $current_plan = [ 'id' => $freeSubscription->id, 'name' => $freeSubscription->subscribe->name, @@ -104,18 +86,22 @@ class HomeController extends Controller ]; } - $latestSubscription = $user->userSubscribers()->latest()->first(); + $latestSubscription = $user->userSubscribers() + ->where('expired_at', '>=', now()) + ->latest('expired_at') + ->first(); $expiredDays = UserSubscriber::query()->where('user_id', $user->id)->where('expired_at', '>=', now())->get()->sum(function ($subscriber) {return $subscriber->expired_at->diffInDays(now());}); - $purchase_token = $latestSubscription?->purchase_token; - $current_plan = [ - 'id' => $latestSubscription->id, - 'name' => $latestSubscription->subscribe->name ?? 'Subscription', - 'price' => $latestSubscription->subscribe->price ?? 100, - 'expired_day' => $expiredDays, - 'is_free' => false - ]; + if ($latestSubscription) { + $current_plan = [ + 'id' => $latestSubscription->id, + 'name' => $latestSubscription->subscribe->name ?? 'Subscription', + 'price' => $latestSubscription->subscribe->price ?? 100, + 'expired_day' => $expiredDays, + 'is_free' => (bool) $latestSubscription->is_free + ]; + } $unread_notifications_count = Notification::unreadForUser($user->id)->count(); @@ -128,4 +114,32 @@ class HomeController extends Controller 'unread_notifications_count' => $unread_notifications_count, ]); } + + private function refreshMarketSubscription($user): void + { + $bazaarSubscription = $user->userSubscribers() + ->whereNotNull('purchase_token') + ->whereNotNull('subscription_id') + ->where(function ($query) { + $query->where('market_provider', 'bazaar') + ->orWhereNull('market_provider'); + }) + ->latest() + ->first(); + + if ($bazaarSubscription && $this->marketVerifier->refreshBazaarSubscriber($bazaarSubscription)) { + return; + } + + $myketSubscription = $user->userSubscribers() + ->where('market_provider', 'myket') + ->whereNotNull('purchase_token') + ->whereNotNull('product_id') + ->latest() + ->first(); + + if ($myketSubscription) { + $this->marketVerifier->refreshMyketSubscriber($myketSubscription); + } + } } diff --git a/app/Http/Controllers/api/SubscribePlanController.php b/app/Http/Controllers/api/SubscribePlanController.php index ea2caec..0394db9 100644 --- a/app/Http/Controllers/api/SubscribePlanController.php +++ b/app/Http/Controllers/api/SubscribePlanController.php @@ -6,6 +6,7 @@ use App\Http\Controllers\Controller; use App\Models\PaymentTransaction; use App\Models\SubscribePlan; use App\Models\UserSubscriber; +use App\Services\AppMarketPurchaseVerifier; use App\Traits\BaseApiResponse; use Carbon\Carbon; use Illuminate\Http\Request; @@ -17,6 +18,10 @@ class SubscribePlanController extends Controller { use BaseApiResponse; + public function __construct(private AppMarketPurchaseVerifier $marketVerifier) + { + } + public function index() { $subscribePlans = SubscribePlan::query()->where('is_active', true)->get()->map(function ($q) { @@ -104,8 +109,6 @@ class SubscribePlanController extends Controller public function subscribe_new(Request $request) { - $package_name_bazzar = 'com.razzaghi.lawbook.android'; - $request->validate([ 'subscribe_plan_id' => 'required|exists:subscribe_plans,id', 'subscription_id' => 'nullable', @@ -149,23 +152,13 @@ class SubscribePlanController extends Controller return $this->failed(null, ['title' => 'Subscribe Plan', 'message' => 'Invalid subscription details.']); } - $url = "https://pardakht.cafebazaar.ir/devapi/v2/api/applications/$package_name_bazzar/subscriptions/$subscription_id/purchases/$purchase_token"; - - $client = new \GuzzleHttp\Client(); try { - $response = $client->get($url, [ - 'headers' => [ - 'CAFEBAZAAR-PISHKHAN-API-SECRET' => 'eyJhbGciOiJIUzI1NiIsImtpZCI6ImFuY2llbnQiLCJ0eXAiOiJKV1QifQ.eyJpc3MiOiJuYXNoZXItcGlzaGtoYW4tYXBpIiwiaWF0IjoxNzQwMjQ3NTMzLCJleHAiOjQ4OTM4NDc1MzMsImFwaV9hZ2VudF9pZCI6MzQ2NX0.UCrr3IHxCqn77ckxfnaubrsyCfrhPm18gJgyg1qNqwA', - ] - ]); + $bazaarExpiredAt = $this->marketVerifier->verifyBazaarSubscription($subscription_id, $purchase_token); - $data = json_decode($response->getBody(), true); - if (!isset($data['validUntilTimestampMsec']) || $data['validUntilTimestampMsec'] < now()->timestamp * 1000) { + if (!$bazaarExpiredAt) { return $this->failed(null, ['title' => 'Subscribe Plan', 'message' => 'Invalid or expired subscription.']); } - $bazaarExpiredAt = Carbon::createFromTimestampMs($data['validUntilTimestampMsec']); - $expiredAt = $activeSubscription ? $activeSubscription->expired_at->addDays($subscribePlan->expired_day) : $bazaarExpiredAt; $user->userSubscribers()->whereHas('subscribe', function ($query) { $query->where('is_free', true); @@ -177,6 +170,8 @@ class SubscribePlanController extends Controller 'expired_at' => $expiredAt, 'subscription_id' => $subscription_id, 'purchase_token' => $purchase_token, + 'market_provider' => 'bazaar', + 'last_verified_at' => now(), 'is_free' => false ]); @@ -187,6 +182,69 @@ class SubscribePlanController extends Controller } } + public function subscribeMyket(Request $request) + { + $request->validate([ + 'subscribe_plan_id' => 'required|exists:subscribe_plans,id', + 'sku_id' => 'required|string', + 'token' => 'required_without_all:tokenId,token_id,purchase_token|string', + 'tokenId' => 'required_without_all:token,token_id,purchase_token|string', + 'token_id' => 'required_without_all:token,tokenId,purchase_token|string', + 'purchase_token' => 'required_without_all:token,tokenId,token_id|string', + ]); + + $subscribePlan = SubscribePlan::findOrFail($request->subscribe_plan_id); + $user = auth()->user(); + $token = $request->input('token') + ?? $request->input('tokenId') + ?? $request->input('token_id') + ?? $request->input('purchase_token'); + + if ($subscribePlan->is_free) { + return $this->failed(null, ['title' => 'Subscribe Plan', 'message' => 'Invalid subscription details.']); + } + + try { + $purchase = $this->marketVerifier->verifyMyketProduct($request->sku_id, $token); + + if (!$purchase) { + return $this->failed(null, ['title' => 'Subscribe Plan', 'message' => 'Invalid or failed Myket purchase.']); + } + + $expiredAt = $purchase['purchased_at']->copy()->addDays($subscribePlan->expired_day); + + if ($expiredAt->lessThan(now())) { + return $this->failed(null, ['title' => 'Subscribe Plan', 'message' => 'Invalid or expired subscription.']); + } + + $user->userSubscribers()->whereHas('subscribe', function ($query) { + $query->where('is_free', true); + })->update(['expired_at' => now()]); + + $user->userSubscribers()->updateOrCreate( + [ + 'market_provider' => 'myket', + 'purchase_token' => $token, + ], + [ + 'subscribe_plan_id' => $subscribePlan->id, + 'expired_at' => $expiredAt, + 'subscription_id' => $request->sku_id, + 'product_id' => $request->sku_id, + 'purchased_at' => $purchase['purchased_at'], + 'last_verified_at' => now(), + 'is_free' => false, + ] + ); + + return $this->success(null, 'Subscribe Plan', 'Myket subscription successfully activated.'); + } catch (\Exception $e) { + Log::error('Error in Myket subscription', ['Error' => $e->getMessage()]); + + return $this->failed(null, ['title' => 'Subscribe Plan', 'message' => 'Failed to verify Myket subscription.']); + } + } + public function paymentCallback(Request $request) { try { diff --git a/app/Models/UserSubscriber.php b/app/Models/UserSubscriber.php index 14042d0..669e663 100644 --- a/app/Models/UserSubscriber.php +++ b/app/Models/UserSubscriber.php @@ -15,6 +15,10 @@ class UserSubscriber extends Model 'expired_at', 'subscription_id', 'purchase_token', + 'market_provider', + 'product_id', + 'purchased_at', + 'last_verified_at', 'is_free' ]; @@ -35,5 +39,7 @@ class UserSubscriber extends Model protected $casts = [ 'expired_at' => 'datetime', + 'purchased_at' => 'datetime', + 'last_verified_at' => 'datetime', ]; } diff --git a/app/Services/AppMarketPurchaseVerifier.php b/app/Services/AppMarketPurchaseVerifier.php new file mode 100644 index 0000000..64a8599 --- /dev/null +++ b/app/Services/AppMarketPurchaseVerifier.php @@ -0,0 +1,117 @@ + config('services.app_markets.bazaar_secret'), + ])->get($url); + + if (!$response->successful()) { + Log::error('Failed to verify Bazaar subscription', [ + 'status' => $response->status(), + 'body' => $response->body(), + ]); + + return null; + } + + $validUntil = $response->json('validUntilTimestampMsec'); + + if (!$validUntil || $validUntil < now()->timestamp * 1000) { + return null; + } + + return Carbon::createFromTimestampMs($validUntil); + } + + public function refreshBazaarSubscriber(UserSubscriber $subscriber): bool + { + if (!$subscriber->subscription_id || !$subscriber->purchase_token) { + return false; + } + + $expiredAt = $this->verifyBazaarSubscription($subscriber->subscription_id, $subscriber->purchase_token); + + if (!$expiredAt) { + $subscriber->update(['expired_at' => now()]); + + return false; + } + + $subscriber->update([ + 'expired_at' => $expiredAt, + 'last_verified_at' => now(), + ]); + + return true; + } + + public function verifyMyketProduct(string $skuId, string $token): ?array + { + $packageName = config('services.app_markets.package_name'); + $url = "https://developer.myket.ir/api/partners/applications/{$packageName}/purchases/products/{$skuId}/verify"; + + $response = Http::withHeaders([ + 'X-Access-Token' => config('services.app_markets.myket_access_token'), + ])->post($url, [ + 'tokenId' => $token, + ]); + + if (!$response->successful()) { + Log::error('Failed to verify Myket purchase', [ + 'status' => $response->status(), + 'body' => $response->body(), + ]); + + return null; + } + + $data = $response->json(); + + if (($data['purchaseState'] ?? null) !== 0 || empty($data['purchaseTime'])) { + return null; + } + + return [ + 'data' => $data, + 'purchased_at' => Carbon::createFromTimestampMs($data['purchaseTime']), + ]; + } + + public function refreshMyketSubscriber(UserSubscriber $subscriber): bool + { + if (!$subscriber->product_id || !$subscriber->purchase_token || !$subscriber->subscribe) { + return false; + } + + $purchase = $this->verifyMyketProduct($subscriber->product_id, $subscriber->purchase_token); + + if (!$purchase) { + $subscriber->update(['expired_at' => now()]); + + return false; + } + + $expiredAt = $purchase['purchased_at']->copy()->addDays($subscriber->subscribe->expired_day); + + $subscriber->update([ + 'purchased_at' => $purchase['purchased_at'], + 'expired_at' => $expiredAt, + 'last_verified_at' => now(), + ]); + + return $expiredAt->greaterThanOrEqualTo(now()); + } +} diff --git a/config/services.php b/config/services.php index 0ace530..2a38f56 100644 --- a/config/services.php +++ b/config/services.php @@ -31,4 +31,11 @@ return [ 'region' => env('AWS_DEFAULT_REGION', 'us-east-1'), ], + 'app_markets' => [ + 'package_name' => env('APP_MARKET_PACKAGE_NAME', 'com.razzaghi.lawbook.android'), + 'myket_access_token' => env('MYKET_ACCESS_TOKEN', 'd9f9207b-72c9-438e-9ff1-0b44e24b2612'), + 'myket_public_key' => env('MYKET_PUBLIC_KEY', 'MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCg3ZcL+tvifbjvSbo1ZKbG0Zw7PwVEtkiP9yCwpn6J9pxs53ZO5dqWDc2u8pewPPFxXbi/sUrbdpuia+l2raSO+8ncp5loA8r/dCRww/qjtWmcXhqGEZWfb9dsh8VduNVjBBva1EcePL63L3PbLgnj+ty/gLd03o+H+yOJbFc/dwIDAQAB'), + 'bazaar_secret' => env('BAZAAR_API_SECRET', 'eyJhbGciOiJIUzI1NiIsImtpZCI6ImFuY2llbnQiLCJ0eXAiOiJKV1QifQ.eyJpc3MiOiJuYXNoZXItcGlzaGtoYW4tYXBpIiwiaWF0IjoxNzQwMjQ3NTMzLCJleHAiOjQ4OTM4NDc1MzMsImFwaV9hZ2VudF9pZCI6MzQ2NX0.UCrr3IHxCqn77ckxfnaubrsyCfrhPm18gJgyg1qNqwA'), + ], + ]; diff --git a/database/migrations/2026_05_16_000001_add_market_fields_to_user_subscribers_table.php b/database/migrations/2026_05_16_000001_add_market_fields_to_user_subscribers_table.php new file mode 100644 index 0000000..a1863d9 --- /dev/null +++ b/database/migrations/2026_05_16_000001_add_market_fields_to_user_subscribers_table.php @@ -0,0 +1,30 @@ +string('market_provider')->nullable()->after('purchase_token'); + $table->string('product_id')->nullable()->after('market_provider'); + $table->timestamp('purchased_at')->nullable()->after('product_id'); + $table->timestamp('last_verified_at')->nullable()->after('purchased_at'); + }); + } + + public function down(): void + { + Schema::table('user_subscribers', function (Blueprint $table) { + $table->dropColumn([ + 'market_provider', + '1', + 'purchased_at', + 'last_verified_at', + ]); + }); + } +}; diff --git a/routes/api.php b/routes/api.php index d5a8bee..f181933 100644 --- a/routes/api.php +++ b/routes/api.php @@ -57,6 +57,7 @@ Route::prefix('v1')->middleware('auth:sanctum')->group(function () { Route::get('subscribe-plans', [SubscribePlanController::class, 'index']); Route::post('subscribe-plan-user', [SubscribePlanController::class, 'subscribe']); Route::post('subscribe-plan-user-new', [SubscribePlanController::class, 'subscribe_new']); + Route::post('subscribe-plan-user-myket', [SubscribePlanController::class, 'subscribeMyket']); Route::get('subscribe-plan-current', [SubscribePlanController::class, 'current']); Route::post('pay',[PayController::class, 'pay']); @@ -69,4 +70,3 @@ Route::prefix('v1')->middleware('auth:sanctum')->group(function () { Route::post('suggestions',[SuggestionController::class,'index']); }); -