feat(giftcard): add customer giftcard/mine endpoint; fix admin/app password digit normalization

This commit is contained in:
2026-09-09 16:57:20 +03:30
parent 741faa38fb
commit 0fa2b4d1f5
8 changed files with 243 additions and 8 deletions
@@ -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
View File
@@ -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
+5
View File
@@ -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');
}
}