validate([ 'per_page' => 'integer', 'mobile' => 'string', 'email' => 'string', 'package_name' => 'string', ]); $users = User::when(isset($data['package_name']), fn ($q) => $q->whereHas('packageNames', fn ($q2) => $q2->where('name', $data['package_name']))) ->when(isset($data['mobile']), fn ($q) => $q->where('mobile', $data['mobile'])) ->when(isset($data['email']), fn ($q) => $q->where('email', $data['email'])) ->paginate($data['per_page'] ?? 30); return $users; } /** * List all subscription purchases (transactions) across users, with * filters for email, mobile (phone), package name and payment source * (gateway). Optionally narrowed to a date/time window (dates as Y-m-d, * times as H:i or H:i:s, both in the app timezone) used by the admin * accounting section. Returns paginated results with the related user, * product and package eager-loaded for display in the admin panel. */ public function purchases(Request $request) { $data = $request->validate([ 'per_page' => 'integer', 'email' => 'string|nullable', 'mobile' => 'string|nullable', 'package_name' => 'string|nullable', // payment source: accepts a gateway name (asanpardakht, zarinpal, // digipay, cafe, myket) or its numeric code 'gateway' => 'string|nullable', 'status' => 'integer|nullable', 'product_id' => 'integer|nullable', 'date_from' => 'date_format:Y-m-d|nullable', 'date_to' => 'date_format:Y-m-d|nullable', 'time_from' => ['nullable', 'regex:/^\d{2}:\d{2}(:\d{2})?$/'], 'time_to' => ['nullable', 'regex:/^\d{2}:\d{2}(:\d{2})?$/'], ]); // Resolve the payment source to its stored integer code $gateway = null; if (!empty($data['gateway'])) { $gateway = Transaction::GATEWAYS[$data['gateway']] ?? (is_numeric($data['gateway']) ? (int) $data['gateway'] : null); } // Date/time window on created_at. A date without a time covers the // whole day; a time narrows the bound to the exact datetime. Times // only apply alongside their date (mirrors the admin panel filters). $normalizeTime = fn ($time) => $time && strlen($time) === 5 ? $time . ':00' : $time; $from = !empty($data['date_from']) ? $data['date_from'] . ' ' . ($normalizeTime($data['time_from'] ?? null) ?? '00:00:00') : null; $to = !empty($data['date_to']) ? $data['date_to'] . ' ' . ($normalizeTime($data['time_to'] ?? null) ?? '23:59:59') : null; $purchases = Transaction::with(['user', 'product.packageName']) ->when(!empty($data['email']), fn ($q) => $q->whereHas('user', fn ($u) => $u->where('email', 'like', '%' . $data['email'] . '%'))) ->when(!empty($data['mobile']), fn ($q) => $q->whereHas('user', fn ($u) => $u->where('mobile', 'like', '%' . $data['mobile'] . '%'))) ->when(!empty($data['package_name']), fn ($q) => $q->whereHas('product.packageName', fn ($p) => $p->where('name', $data['package_name']))) ->when(!empty($data['product_id']), fn ($q) => $q->where('product_id', $data['product_id'])) ->when(!is_null($gateway), fn ($q) => $q->where('gateway', $gateway)) ->when(isset($data['status']), fn ($q) => $q->where('status', $data['status'])) ->when($from, fn ($q) => $q->where('created_at', '>=', $from)) ->when($to, fn ($q) => $q->where('created_at', '<=', $to)) ->latest() ->paginate($data['per_page'] ?? 30) ->withQueryString(); return $purchases; } public function getUserTransactions(Request $request, $identifier) { $data = $request->validate([ 'per_page' => 'integer' ]); if (!$user = $this->findUserByIdentifier($identifier)) { return response()->json([ 'message' => 'user not found' ], 404); } $transactions = $user->transactions()->paginate($data['per_page'] ?? 30); return $transactions; } public function getUserStatus(Request $request, $identifier) { $data = $request->validate([ 'package_name' => 'string' ]); if (isset($data['package_name']) && !$packageName = PackageName::with('products')->where('name', $data['package_name'])->first()) { return response()->json([ 'message' => 'package name not found' ], 404); } if (!$user = User::with([ 'products' => fn ($q) => $q->when(isset($data['package_name']), fn ($q1) => $q1->where('package_name_id', $packageName->id)), 'products.packageName' ])->where(function($query) use ($identifier) { $this->applyIdentifierCondition($query, $identifier); })->first()) { return response()->json([ 'message' => 'user not found' ], 404); } return $user; } public function getUserStatusV1(Request $request, $identifier) { $data = $request->validate([ 'package_name' => 'string' ]); $userIdentifier = $identifier; if (isset($data['package_name']) && !$packageName = PackageName::with('products') ->where('name', $data['package_name'])->first()) { return response()->json([ 'message' => 'package name not found' ], 404); } // For OldPurchase, we need to determine if identifier is mobile or email if ($this->isMobileNumber($identifier)) { $mobile = preg_replace('/09/', '989', $identifier, 1); $query = OldPurchase::when(isset($data['package_name']), fn ($q) => $q->where('package_name', $data['package_name'])) ->where('mobile', $mobile); } else { // Assuming OldPurchase might have email field or we need to find user first if (!$user = $this->findUserByIdentifier($identifier)) { return response()->json([ 'message' => 'user not found' ], 404); } $query = OldPurchase::when(isset($data['package_name']), fn ($q) => $q->where('package_name', $data['package_name'])) ->where('mobile', preg_replace('/09/', '989', $user->mobile, 1)); } if (!$query->first()) { return response()->json([ 'message' => 'user is not paid' ], 400); } return response()->json([ 'message' => 'user is paid' ]); } public function subscribeUser(Request $request, $identifier) { $data = $request->validate([ 'product_id' => 'integer|required' ]); if (!$user = $this->findUserByIdentifier($identifier)) { return response()->json([ 'message' => 'user not found' ], 404); } if (!$product = Product::where('id', $data['product_id'])->first()) { return response()->json([ 'message' => 'product not found' ], 404); } $product->buy($user); return response()->json([ 'message' => 'subscribed successfuly' ]); } /** * List users who reached the referral invite target (کد معرف) and have * not yet been granted their free-month subscription. */ public function pendingReferralRewards(Request $request) { $target = User::REFERRAL_SUBSCRIPTION_TARGET; // Use has() (a correlated subquery in WHERE) rather than having() on the // withCount alias — Postgres does not allow select aliases in HAVING. $users = User::withCount('referrals') ->whereNull('referral_subscription_granted_at') ->has('referrals', '>=', $target) ->orderByDesc('referrals_count') ->get(['id', 'uuid', 'first_name', 'last_name', 'email', 'mobile', 'referral_code']); return response()->json([ 'subscription_target' => $target, 'data' => $users->map(fn ($u) => [ 'id' => $u->id, 'uuid' => $u->uuid, 'name' => trim($u->first_name . ' ' . $u->last_name), 'email' => $u->email, 'mobile' => $u->mobile, 'referral_code' => $u->referral_code, 'successful_invites' => $u->referrals_count, ]), ]); } /** * Grant the free-month subscription to a user who reached the referral * invite target. Defaults to the meditation package's monthly product, * overridable via product_id. Marks the grant so it happens only once. */ public function fulfillReferralReward(Request $request, $userId) { $data = $request->validate([ 'product_id' => 'integer|nullable', ]); if (!$user = User::find($userId)) { return response()->json(['message' => 'user not found'], 404); } if ($user->referral_subscription_granted_at) { return response()->json(['message' => 'referral subscription already granted'], 400); } if ($user->referrals()->count() < User::REFERRAL_SUBSCRIPTION_TARGET) { return response()->json(['message' => 'user has not reached the referral target yet'], 400); } // Resolve which monthly product to grant if (!empty($data['product_id'])) { $product = Product::where('id', $data['product_id'])->first(); } else { $package = PackageName::where('name', User::REFERRAL_PACKAGE_NAME)->first(); $product = $package ? Product::where('package_name_id', $package->id) ->where('type', Product::TYPES['monthly']) ->first() : null; } if (!$product) { return response()->json(['message' => 'monthly product not found'], 404); } $product->buy($user); $user->referral_subscription_granted_at = now(); $user->save(); return response()->json([ 'message' => 'referral subscription granted', 'user_id' => $user->id, 'product_id' => $product->id, ]); } public function unsubscribeUser(Request $request, $identifier) { $data = $request->validate([ 'product_id' => 'integer|required' ]); if (!$user = $this->findUserByIdentifier($identifier)) { return response()->json([ 'message' => 'user not found' ], 404); } if (!$product = Product::where('id', $data['product_id'])->first()) { return response()->json([ 'message' => 'product not found' ], 404); } $user->products()->updateExistingPivot($product, ['expire_at' => now()->format('Y-m-d H:i:s')]); return response()->json([ 'message' => 'unsubscribed successfuly' ]); } public function deleteUser(Request $request, $identifier) { if (!$user = $this->findUserByIdentifier($identifier)) { return response()->json([ 'message' => 'user not found' ], 404); } $user->delete(); return response()->json([ 'message' => 'user deleted' ]); } public function updateUserProfile(Request $request, $identifier) { $data = $request->validate([ 'first_name' => 'string', 'last_name' => 'string', 'email' => 'email', 'mobile' => [new MobileNumber, 'string'], 'avatar' => 'image' ]); if (!$subjectUser = $this->findUserByIdentifier($identifier)) { return response()->json([ 'message' => 'user not found' ], 404); } if (isset($data['email'])) { $user = User::where('email', $data['email'])->first(); if ($user && $data['email'] != $subjectUser->email) { return response()->json([ 'message' => 'user with given email exists' ], 400); } } if (isset($data['mobile'])) { $user = User::where('mobile', $data['mobile'])->first(); if ($user && $data['mobile'] != $subjectUser->mobile) { return response()->json([ 'message' => 'user with given mobile exists' ], 400); } } if (isset($data['avatar'])) { Storage::disk('public')->put("avatars/$subjectUser->uuid.png", file_get_contents($data['avatar']->path())); $data['avatar'] = "storage/avatars/$subjectUser->uuid.png"; } $subjectUser->update($data); return response()->json([ 'message' => 'user updated' ]); } /** * Helper method to find user by mobile or email */ private function findUserByIdentifier($identifier) { return User::where(function($query) use ($identifier) { $this->applyIdentifierCondition($query, $identifier); })->first(); } /** * Helper method to apply condition for mobile or email */ private function applyIdentifierCondition($query, $identifier) { if ($this->isMobileNumber($identifier)) { $query->where('mobile', $identifier); } else { $query->where('email', $identifier); } } /** * Helper method to check if identifier is a mobile number */ private function isMobileNumber($identifier) { // Simple check for mobile number pattern (starts with 09 or +98 or 989) return preg_match('/^(09|\+98|989)\d{9}$/', $identifier); } }