feat(giftcard): add customer giftcard/mine endpoint; fix admin/app password digit normalization
This commit is contained in:
@@ -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 غیرخالی بفرست
|
||||
|
||||
@@ -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'] !== '') {
|
||||
|
||||
@@ -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()) {
|
||||
|
||||
@@ -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'])
|
||||
|
||||
+22
-2
@@ -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
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+6
-6
@@ -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,
|
||||
],
|
||||
],
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user