From 0fa2b4d1f597b9faf68de3e5602c13b191833936 Mon Sep 17 00:00:00 2001 From: safavimohadeseh78 Date: Wed, 9 Sep 2026 16:57:20 +0330 Subject: [PATCH] feat(giftcard): add customer giftcard/mine endpoint; fix admin/app password digit normalization --- .../abshode-domain-lookup/learned-notes.md | 3 + .../Controllers/Admin/CustomerController.php | 2 + .../Controllers/Admin/GiftCardController.php | 198 ++++++++++++++++++ app/Http/Controllers/Auth/AuthController.php | 6 + app/Models/Customer.php | 24 ++- app/Models/GiftCardCodes.php | 5 + config/auth.php | 12 +- routes/customer.php | 1 + 8 files changed, 243 insertions(+), 8 deletions(-) diff --git a/.cursor/skills/abshode-domain-lookup/learned-notes.md b/.cursor/skills/abshode-domain-lookup/learned-notes.md index 643ca8f..7b83ba4 100644 --- a/.cursor/skills/abshode-domain-lookup/learned-notes.md +++ b/.cursor/skills/abshode-domain-lookup/learned-notes.md @@ -6,3 +6,6 @@ - 2026-09-06 | گیفت‌کارت | `database/migrations/2026_09_06_000001_create_giftcards_tables.php` | create جداول giftcards و giftcard_codes اگر نباشند؛ drop در down ندارد - 2026-09-08 | خطای «محدودیت فیلد قیمت» در گیفت‌کارت | `app/Exceptions/Handler.php` + `GiftCardController::redeem` | پیام عمومی MySQL ۱۲۶۴ (Out of range) است؛ معمولاً `amount`/`total=count*amount` از ظرفیت ستون بیشتر است، نه باگ فرم ادمین - 2026-09-08 | widen مبلغ گیفت‌کارت | `migrations/2026_09_08_000001_widen_giftcard_amount_columns.php` + `deploy_safe_schema.sql` | `amount`/`total` → DECIMAL(20,4) تا مبالغ ≥۱۰۰ میلیون ریال جا شود +- 2026-09-08 | لیست گیفت کاربر | `GiftCardController::mine` + `GET customer/giftcard/mine` + `GiftCardCodes::giftcard` | available=اختصاصی+عمومی تک‌کد use_once=0؛ history از assigned_to +- 2026-09-09 | «کد قابل استفاده» خالی در اپ | `routes/customer.php` + `GiftCardController::mine` | مسیر `GET customer/giftcard/mine` هنوز کامیت/دیپلوی نشده؛ سرور واقعی ۴۰۴ می‌دهد (use و me → ۴۰۱) پس لیست همیشه خالی است +- 2026-09-09 | رمز ادمین ≠ لاگین اپ | `Customer::setPasswordAttribute` + `AuthController` + `updateAdmin` | قبل از هش/لاگین ارقام فارسی→انگلیسی؛ jwt hash=false؛ آنلاین فقط password غیرخالی بفرست diff --git a/app/Http/Controllers/Admin/CustomerController.php b/app/Http/Controllers/Admin/CustomerController.php index 954c2c4..2d610a6 100644 --- a/app/Http/Controllers/Admin/CustomerController.php +++ b/app/Http/Controllers/Admin/CustomerController.php @@ -150,6 +150,8 @@ class CustomerController extends SuperController if (array_key_exists('password', $data) && trim((string) ($data['password'] ?? '')) === '') { unset($data['password']); + } elseif (array_key_exists('password', $data)) { + $data['password'] = Customer::normalizeCredentialDigits(trim((string) $data['password'])); } if (isset($data['mobile']) && ! is_null($data['mobile']) && $data['mobile'] !== '') { diff --git a/app/Http/Controllers/Admin/GiftCardController.php b/app/Http/Controllers/Admin/GiftCardController.php index 44dba0e..6b44415 100644 --- a/app/Http/Controllers/Admin/GiftCardController.php +++ b/app/Http/Controllers/Admin/GiftCardController.php @@ -194,6 +194,204 @@ class GiftCardController extends Controller ]); } + /** + * لیست کدهای قابل‌استفاده (اختصاصی + عمومی تک‌کد) و تاریخچهٔ مصرف‌شدهٔ مشتری. + */ + public function mine(Request $request) + { + $user = auth()->user(); + if (! $user) { + throw new UnprocessableEntityHttpException('auth failed!'); + } + + if (! app(ConfigService::class)->getBool('has_page_gift_card', false)) { + throw new UnprocessableEntityHttpException('قابلیت کارت هدیه غیرفعال است.'); + } + + $customerId = (int) $user->id; + $mobile = $user->mobile ?? null; + $available = []; + $seenCodes = []; + + $personalGifts = GiftCard::query() + ->where(function ($q) use ($customerId, $mobile) { + $q->where('customer_id', $customerId); + if ($mobile) { + $q->orWhere('mobile', $mobile); + } + }) + ->where(function ($q) { + $q->whereNull('sts')->orWhere('sts', '!=', 3); + }) + ->where(function ($q) { + $q->whereNull('expiry_date')->orWhere('expiry_date', '>', now()); + }) + ->with(['giftcardcodes' => function ($q) { + $q->where('sts', 1); + }]) + ->orderByDesc('id') + ->get(); + + foreach ($personalGifts as $gift) { + foreach ($gift->giftcardcodes as $code) { + if ($this->isCodeExpired($code, $gift)) { + continue; + } + if (isset($seenCodes[$code->code])) { + continue; + } + $seenCodes[$code->code] = true; + $available[] = $this->mapCustomerGiftItem($gift, $code, 'personal'); + } + } + + $publicGifts = GiftCard::query() + ->where(function ($q) { + $q->whereNull('customer_id')->orWhere('customer_id', 0); + }) + ->where(function ($q) { + $q->whereNull('mobile')->orWhere('mobile', ''); + }) + ->where('use_once', 0) + ->where(function ($q) { + $q->whereNull('sts')->orWhere('sts', '!=', 3); + }) + ->where(function ($q) { + $q->whereNull('expiry_date')->orWhere('expiry_date', '>', now()); + }) + ->with('giftcardcodes') + ->orderByDesc('id') + ->get(); + + foreach ($publicGifts as $gift) { + if ((int) $gift->count > 0 && (int) $gift->used_count >= (int) $gift->count) { + continue; + } + + $codes = $gift->giftcardcodes + ->filter(function ($code) use ($gift) { + return (int) $code->sts !== 2 + && (int) $code->sts !== 3 + && ! $this->isCodeExpired($code, $gift); + }) + ->values(); + + // عمومی: فقط پویش تک‌کد مشترک (جلوگیری از افشای بچ تصادفی) + if ($codes->count() !== 1 && empty($gift->unique_code)) { + continue; + } + + $code = $codes->first(); + if (! $code) { + continue; + } + if ($this->customerAlreadyUsedCode($code, $customerId, $gift)) { + continue; + } + if (isset($seenCodes[$code->code])) { + continue; + } + $seenCodes[$code->code] = true; + $available[] = $this->mapCustomerGiftItem($gift, $code, 'public'); + } + + $history = []; + $historyCodes = GiftCardCodes::query() + ->whereNotNull('assigned_to') + ->where('assigned_to', '!=', '') + ->where(function ($q) use ($customerId) { + $id = (string) $customerId; + $q->where('assigned_to', $customerId) + ->orWhere('assigned_to', $id) + ->orWhere('assigned_to', '['.$id.']') + ->orWhere('assigned_to', 'like', '%"'.$id.'"%') + ->orWhere('assigned_to', 'like', '%['.$id.',%') + ->orWhere('assigned_to', 'like', '%,'.$id.']%') + ->orWhere('assigned_to', 'like', '%,'.$id.',%'); + }) + ->with('giftcard') + ->orderByDesc('used_at') + ->orderByDesc('updated_at') + ->limit(100) + ->get(); + + foreach ($historyCodes as $code) { + $gift = $code->giftcard; + if (! $gift || ! $this->customerAlreadyUsedCode($code, $customerId, $gift)) { + continue; + } + $history[] = $this->mapCustomerGiftItem($gift, $code, $this->resolveCodeScope($gift, $customerId, $mobile)); + } + + return response()->json([ + 'available' => $available, + 'history' => $history, + ]); + } + + private function isCodeExpired(GiftCardCodes $code, GiftCard $gift): bool + { + if ($code->expiry_date && $code->expiry_date <= now()) { + return true; + } + if ($gift->expiry_date && $gift->expiry_date <= now()) { + return true; + } + + return false; + } + + private function customerAlreadyUsedCode(GiftCardCodes $code, int $customerId, GiftCard $gift): bool + { + $assigned = $code->assigned_to; + if ($assigned === null || $assigned === '') { + return false; + } + + if ((int) $gift->unit === 3 || (is_string($assigned) && str_starts_with(trim($assigned), '['))) { + $ids = json_decode($assigned, true); + if (! is_array($ids)) { + return false; + } + + return in_array($customerId, $ids, false) || in_array((string) $customerId, $ids, false); + } + + return (string) $assigned === (string) $customerId; + } + + private function resolveCodeScope(GiftCard $gift, int $customerId, ?string $mobile): string + { + if ((int) $gift->customer_id === $customerId) { + return 'personal'; + } + if ($mobile && $gift->mobile && (string) $gift->mobile === (string) $mobile) { + return 'personal'; + } + + return 'public'; + } + + private function mapCustomerGiftItem(GiftCard $gift, GiftCardCodes $code, string $scope): array + { + $unit = (int) ($code->unit ?? $gift->unit); + + return [ + 'id' => $code->id, + 'giftcard_id' => $gift->id, + 'code' => $code->code, + 'name' => $gift->name, + 'description' => $gift->description, + 'amount' => $code->amount ?? $gift->amount, + 'unit' => $unit, + 'kind' => $unit === 3 ? 'discount' : 'gift', + 'scope' => $scope, + 'sts' => (int) $code->sts, + 'expiry_date' => $code->expiry_date ?? $gift->expiry_date, + 'used_at' => $code->used_at, + ]; + } + public function useDiscount(Request $request) { if (! app(ConfigService::class)->isStore()) { diff --git a/app/Http/Controllers/Auth/AuthController.php b/app/Http/Controllers/Auth/AuthController.php index 4d3999e..49e0cac 100644 --- a/app/Http/Controllers/Auth/AuthController.php +++ b/app/Http/Controllers/Auth/AuthController.php @@ -54,6 +54,12 @@ class AuthController extends Controller protected function _loginWithPassword($credentials, $provider) { + if (isset($credentials['password'])) { + $credentials['password'] = Customer::normalizeCredentialDigits( + trim((string) $credentials['password']) + ); + } + if ( $provider === 'customer' && isset($credentials['mobile'], $credentials['password']) diff --git a/app/Models/Customer.php b/app/Models/Customer.php index 0b2b70c..321dcee 100644 --- a/app/Models/Customer.php +++ b/app/Models/Customer.php @@ -129,12 +129,32 @@ class Customer extends Authenticatable implements JWTSubject ->first(); } -// hash password +// hash password — ارقام فارسی/عربی را قبل از هش انگلیسی کن (لاگین اپ هم انگلیسی می‌فرستد) public function setPasswordAttribute($value){ if ($value === null || $value === '') { return; } - $this->attributes['password'] = Hash::make($value); + $value = trim((string) $value); + if ($value === '') { + return; + } + // اگر از قبل bcrypt است دوباره هش نکن + if (preg_match('/^\$2[ayb]\$.{56}$/', $value) === 1) { + $this->attributes['password'] = $value; + + return; + } + $this->attributes['password'] = Hash::make(self::normalizeCredentialDigits($value)); + } + + /** ارقام فارسی/عربی → لاتین (برای رمز و موبایل در احراز هویت) */ + public static function normalizeCredentialDigits(string $value): string + { + $persian = ['۰', '۱', '۲', '۳', '۴', '۵', '۶', '۷', '۸', '۹']; + $arabic = ['٠', '١', '٢', '٣', '٤', '٥', '٦', '٧', '٨', '٩']; + $latin = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9']; + + return str_replace(array_merge($persian, $arabic), array_merge($latin, $latin), $value); } public function setBirthDateAttribute($value): void diff --git a/app/Models/GiftCardCodes.php b/app/Models/GiftCardCodes.php index b177ffe..cb4b9b8 100644 --- a/app/Models/GiftCardCodes.php +++ b/app/Models/GiftCardCodes.php @@ -40,4 +40,9 @@ class GiftCardCodes extends Model return $this->belongsTo(Customer::class, 'assigned_to', 'id')->select('id', 'name', 'mobile'); } + public function giftcard() + { + return $this->belongsTo(GiftCard::class, 'giftcard_id', 'id'); + } + } diff --git a/config/auth.php b/config/auth.php index 0c0ba79..ab24b0e 100644 --- a/config/auth.php +++ b/config/auth.php @@ -40,15 +40,15 @@ return [ 'driver' => 'session', 'provider' => 'users', ], - 'user' => [ - 'driver' => 'jwt', - 'provider' => 'users', - 'hash' => true, - ], 'customer' => [ 'driver' => 'jwt', 'provider' => 'customers', - 'hash' => true, + 'hash' => false, + ], + 'user' => [ + 'driver' => 'jwt', + 'provider' => 'users', + 'hash' => false, ], ], diff --git a/routes/customer.php b/routes/customer.php index 382559c..dea36b6 100644 --- a/routes/customer.php +++ b/routes/customer.php @@ -162,6 +162,7 @@ Route::middleware(['auth:customer', 'customer.active'])->group(function () use ( Route::post('account/tahesab/trade', [TahesabController::class, 'storeTrade']); //--------------------use gift card + Route::get('giftcard/mine', [GiftCardController::class, 'mine']); Route::post('giftcard/use', [GiftCardController::class, 'useGiftCard']); //offer code