Skip to content

Releases

v2.16.0 — داکر: کل فروشگاه در یک دستور (Docker: the whole shop in one command)

Choose a tag to compare

@MSS1625 MSS1625 released this 05 Nov 13:41

🐳 v2.16.0 — داکر: کل فروشگاه در یک دستور (Docker: the whole shop in one command)

دیجی‌شاپ — فروشگاه آنلاین تجهیزات شبکه | Digi-Shop — Online network-equipment store
Laravel 12 · PHP 8.2+ · Bootstrap 5 RTL · MIT License

🇬🇧 English

🐳 New — Docker: the whole shop in one command

  • docker compose up -d --wait → http://localhost:8080 — the full store (PHP-FPM app, nginx, MySQL 8.4) boots with one command; no PHP, no Composer, no MySQL on the host — only Docker
  • One image, three containers: a multi-stage Dockerfile (composer:2 stage bakes the optimized no-dev autoloader, then php:8.3-fpm-alpine + nginx) runs twice — the app service is php-fpm, the web service is the same image in web role serving static files and proxying PHP to app:9000 by service name; db is the official mysql:8.4 with a health-checked volume
  • The container boots through the project's own installer: the entrypoint copies .env.docker → .env, waits for MySQL to be really ready (healthchecks + a docker/await-db.php retry loop — depends_on alone only guarantees order), then runs the same idempotent php artisan shop:install the project has had since v1.0.0 — restart-safe, no duplicate data
  • Data survives rebuilds in the db-data and storage-data volumes; host port changeable via DIGISHOP_PORT; admin password printed once in docker compose logs app (or set ADMIN_PASSWORD in .env.docker)
  • Tests inside a container, zero host tooling: docker compose run --rm test runs all 304 tests on SQLite in-memory
  • New docker job in CI proves the image on every push and PR: build → healthcheck-gated compose up → homepage smoke test (title + seeded product) → full suite in the test container
  • .dockerignore keeps the image lean and excludes the machine-local public/storage symlink (it would dangle in the container; the entrypoint recreates it each boot)

📊 Numbers

  • Zero migrations, zero PHP code changes — the suite stays at 304 tests (1183 assertions), all green; Pint clean (163 files); Larastan level 5: 0 errors
  • New files: Dockerfile, docker-compose.yml, .dockerignore, .env.docker, docker/nginx.conf, docker/entrypoint.sh, docker/await-db.php; ci.yml gained the docker job (now five jobs + coverage on main)
Seeing it in action

On any machine with Docker:

git clone https://github.com/MSS1625/shop-php.git && cd shop-php
docker compose up -d --wait     # build + boot app/web/db, gated on healthchecks
# → http://localhost:8080

docker compose logs app         # admin password + Laravel logs (stderr)
docker compose run --rm test    # the whole 304-test suite, inside a container
docker compose down             # stop (data stays) · add -v to wipe

On GitHub, after pushing this version:

  • Actions → CI now shows the Docker — build & boot the full stack job beside the test matrix, Pint, Larastan and Coverage: it builds the production image, boots the whole stack with --wait, greps the rendered homepage for the store title and a seeded product, then runs the full suite inside the test container
Quick start without Docker (unchanged)
git clone https://github.com/MSS1625/shop-php.git && cd shop-php
composer install
php artisan shop:install     # env + key + migrate + seed + link (includes WELCOME10)
php artisan admin:create     # interactive admin user
php artisan serve            # → http://localhost:8000

🇮🇷 فارسی

🐳 جدید — داکر: کل فروشگاه در یک دستور

  • docker compose up -d --wait → http://localhost:8080 — کل فروشگاه (اپ PHP-FPM، nginx، MySQL 8.4) با یک دستور بالا می‌آید؛ بدون PHP و Composer و MySQL روی سیستم — فقط خود داکر
  • یک ایمیج، سه کانتینر: Dockerfile چندمرحله‌ای (مرحلهٔ composer:2 که autoloader بهینهٔ بدون-dev را می‌پزد و بعد php:8.3-fpm-alpine + nginx) یک‌بار ساخته و دو بار استفاده می‌شود — سرویس app همان php-fpm است و سرویس web همان ایمیج در نقش وب: فایل‌های استاتیک را مستقیم می‌دهد و PHP را با نام سرویس به app:9000 پاس می‌کند؛ db هم ایمیج رسمی mysql:8.4 با volume دارای healthcheck است
  • کانتینر با نصب‌کنندهٔ خود پروژه بوت می‌شود: entrypoint فایل .env.docker را به .env کپی می‌کند، برای آماده‌شدن واقعی MySQL صبر می‌کند (healthcheckها + حلقهٔ تلاش مجدد docker/await-db.php — depends_on به‌تنهایی فقط ترتیب را تضمین می‌کند) و بعد همان php artisan shop:install idempotent را اجرا می‌کند که از v1.0.0 همراه پروژه بوده — restart بدون خطر، بدون دادهٔ تکراری
  • داده‌ها از rebuild می‌مانند (volumeهای db-data و storage-data)؛ پورت هاست با DIGISHOP_PORT قابل تغییر است؛ رمز مدیر یک‌بار در docker compose logs app چاپ می‌شود (یا ADMIN_PASSWORD را در .env.docker بگذارید)
  • تست داخل کانتینر، بدون هیچ ابزاری روی سیستم: docker compose run --rm test هر ۳۰۴ تست را با SQLite درون‌حافظه اجرا می‌کند
  • جاب جدید docker در CI ایمیج را روی هر push و PR اثبات می‌کند: بیلد ← بوت compose با دروازهٔ healthcheckها ← اسموک‌تست صفحهٔ اول (عنوان + محصول seed شده) ← کل تست‌ها در کانتینر test
  • .dockerignore ایمیج را سبک نگه می‌دارد و سیم‌لینک محلی public/storage را حذف می‌کند (داخل کانتینر می‌شکست؛ entrypoint در هر بوت می‌سازدش)

📊 اعداد

  • صفر مایگریشن و صفر تغییر کد PHP — تست‌ها همان ۳۰۴ تست (۱۱۸۳ ادعا)، همه سبز؛ Pint تمیز (۱۶۳ فایل)؛ لاراستان سطح ۵: صفر خطا
  • فایل‌های جدید: Dockerfile، docker-compose.yml، .dockerignore، .env.docker، docker/nginx.conf، docker/entrypoint.sh، docker/await-db.php؛ جاب docker به ci.yml اضافه شد (حالا پنج جاب + پوشش روی main)
دیدنش در عمل

روی هر سیستمی که داکر دارد:

git clone https://github.com/MSS1625/shop-php.git && cd shop-php
docker compose up -d --wait     # بیلد + بوت app/web/db با دروازهٔ healthcheckها
# ← http://localhost:8080

docker compose logs app         # رمز مدیر + لاگ‌های لاراول (روی stderr)
docker compose run --rm test    # کل ۳۰۴ تست، داخل کانتینر
docker compose down             # توقف (داده‌ها می‌مانند) · با -v پاک می‌شوند

روی گیت‌هاب، بعد از پوش این نسخه:

  • Actions → CI حالا جاب Docker — build & boot the full stack را کنار ماتریس تست، Pint، لاراستان و پوشش نشان می‌دهد: ایمیج production را می‌سازد، کل استک را با --wait بالا می‌آورد، عنوان فروشگاه و یک محصول seed شده را در صفحهٔ رندرشده جستجو می‌کند و بعد کل تست‌ها را داخل کانتینر test اجرا می‌کند
شروع سریع بدون داکر (بدون تغییر)
git clone https://github.com/MSS1625/shop-php.git && cd shop-php
composer install
php artisan shop:install     # env + کلید + مهاجرت + داده اولیه + لینک
php artisan admin:create     # ساخت مدیر (تعاملی)
php artisan serve            # → http://localhost:8000

🤖 v2.15.0 — اتوماسیون CI: لاراستان، بج پوشش، انتشار خودکار (CI Automation: Larastan, Coverage Badge, Auto-Release)

Choose a tag to compare

@MSS1625 MSS1625 released this 03 Nov 18:41
a25ba7f

🤖 v2.15.0 — اتوماسیون CI: لاراستان، بج پوشش، انتشار خودکار (CI Automation: Larastan, Coverage Badge, Auto-Release)

دیجی‌شاپ — فروشگاه آنلاین تجهیزات شبکه | Digi-Shop — Online network-equipment store
Laravel 12 · PHP 8.2+ · Bootstrap 5 RTL · MIT License

🇬🇧 English

🤖 New — CI automation: Larastan, coverage badge, auto-release

  • Larastan (PHPStan for Laravel) level 5 — 0 errors, 0 ignores: the whole codebase (app, config, database, routes, tests) now passes strict static analysis. A new static-analysis job runs it in CI on every push and PR, beside the test matrix (PHP 8.2 / 8.3 / 8.4) and Pint
  • Getting there meant ~35 files got real type annotations instead of suppressed errors: every Eloquent relation declares its generics, factories bind their model, every API Resource declares @mixin of its model — so $order->items, $item->product and $this->id inside resources are statically checked, not magic
  • The analysis caught real things, and they are fixed: cart and API order-item building is now an explicit null-safe loop, AdminUserSeeder reads the admin password via config() (plain env() breaks under config:cache), and the stock ExampleTest placeholder — an always-true assertion — is gone
  • Live coverage badge, zero third-party services: on every push to main a coverage job runs the suite with pcov, scripts/coverage-badge.php (unit-tested) turns the Clover report into a shields.io endpoint JSON and pushes it to the coverage-badge branch — the new Coverage badge in both READMEs reads from there (red < 50% · yellow ≥ 50% · green ≥ 80%). It appears after the first run of the new workflow on main
  • Auto-release: push a tag v* and release.yml runs the whole test suite (a broken tag never ships), extracts the bilingual notes for that version from CHANGELOG.md via scripts/extract-release-notes.php (unit-tested) and creates the GitHub Release — title and body included, no manual copy-paste
  • New composer shortcuts: composer analyse (Larastan) and composer check (tests + Pint + Larastan — the exact gate CI runs)

📊 Numbers

  • 304 tests (1183 assertions) — all green; Pint clean; Larastan level 5: 0 errors, 0 ignores
  • Zero migrations, zero config breaking changes — apply the zip and you're done; shop:install has nothing new to run
  • New files: phpstan.neon.dist, scripts/extract-release-notes.php, scripts/coverage-badge.php, tests/Unit/ExtractReleaseNotesTest.php, tests/Unit/CoverageBadgeTest.php, .github/workflows/release.yml; larastan ^3.14 added to require-dev (composer.lock updated)
Seeing it in action

On your machine:

composer analyse   # vendor/bin/phpstan analyse — level 5, 0 errors
composer check     # tests + Pint + Larastan — the same gate CI runs

On GitHub, after pushing this version to main:

  • Actions → CI now shows four jobs: PHP 8.2 / 8.3 / 8.4 — Tests, Code Style (Pint), Static Analysis (Larastan) and Coverage + Badge
  • when Coverage + Badge finishes, the Coverage badge in the README stops saying "inaccessible" and shows the real percentage — and updates itself on every push to main
  • push a tag and watch Actions → Release: tests run first, then the release appears on the Releases page with the bilingual notes pulled from CHANGELOG.md
Quick start (unchanged)
git clone https://github.com/MSS1625/shop-php.git && cd shop-php
composer install
php artisan shop:install     # env + key + migrate + seed + link (includes WELCOME10)
php artisan admin:create     # interactive admin user
php artisan serve            # → http://localhost:8000

🇮🇷 فارسی

🤖 جدید — اتوماسیون CI: لاراستان، بج پوشش، انتشار خودکار

  • لاراستان (PHPStan مخصوص Laravel) در سطح ۵ — صفر خطا، صفر استثنا: کل کدبیس (app، config، database، routes، tests) تحلیل ایستای سخت‌گیرانه را پاس می‌کند. جاب جدید static-analysis آن را در هر push و PR اجرا می‌کند — کنار ماتریس تست‌ها (PHP 8.2 / 8.3 / 8.4) و Pint
  • برای رسیدن به این نقطه، حدود ۳۵ فایل به‌جای سرکوب‌کردن خطاها تایپ واقعی گرفتند: هر رابطهٔ Eloquent جنریک‌هایش را اعلام می‌کند، فکتوری‌ها مدل‌شان را بایند می‌کنند و هر Resource مربوط به API یک @mixin از مدلش دارد — یعنی $order->items و $item->product و $this->id داخل Resourceها ایستا چک می‌شوند، نه جادویی
  • تحلیل چند ایراد واقعی هم پیدا کرد که همه رفع شدند: ساخت اقلام سبد و سفارشِ API حالا حلقهٔ صریحِ null-safe است، AdminUserSeeder رمز مدیر را از config() می‌خواند (فراخوانی خام env() زیر config:cache می‌شکند) و جانگهدار پیش‌فرض ExampleTest — یک ادعای همیشه-درست — حذف شد
  • بج زندهٔ پوشش، بدون هیچ سرویس خارجی: روی هر push به main جاب پوشش، تست‌ها را با pcov اجرا می‌کند، scripts/coverage-badge.php (تست‌شده) گزارش Clover را به JSON بج shields.io تبدیل و روی شاخهٔ coverage-badge پوش می‌کند — بج جدید Coverage در هر دو README از همان‌جا می‌خواند (قرمز < ۵۰٪ · زرد ≥ ۵۰٪ · سبز ≥ ۸۰٪) و بعد از اولین اجرای ورک‌فلو روی main ظاهر می‌شود
  • انتشار خودکار: تگ v* را پوش کنید تا release.yml اول کل تست‌ها را اجرا کند (تگِ شکسته هرگز رلیز نمی‌شود)، یادداشت‌های دوزبانهٔ همان نسخه را از CHANGELOG.md بکشد و Release گیت‌هاب را با عنوان و متن بسازد — بدون کپی‌وپیست دستی
  • میان‌برهای جدید کامپوزر: composer analyse (لاراستان) و composer check (تست + Pint + لاراستان — دقیقاً همان دروازه‌ای که CI اجرا می‌کند)

📊 اعداد

  • ۳۰۴ تست (۱۱۸۳ ادعا) — همه سبز؛ Pint تمیز؛ لاراستان سطح ۵: صفر خطا، صفر استثنا
  • صفر مایگریشن و صفر تغییر شکنندهٔ کانفیگ — زیپ را اعمال کنید و تمام؛ shop:install چیز جدیدی برای اجرا ندارد
  • فایل‌های جدید: phpstan.neon.dist، scripts/extract-release-notes.php، scripts/coverage-badge.php، tests/Unit/ExtractReleaseNotesTest.php، tests/Unit/CoverageBadgeTest.php، .github/workflows/release.yml؛ لاراستان ^3.14 به require-dev اضافه شد (composer.lock آپدیت شد)
دیدنش در عمل

روی سیستم خودتان:

composer analyse   # vendor/bin/phpstan analyse — سطح ۵، صفر خطا
composer check     # تست + Pint + لاراستان — همان دروازهٔ CI

روی گیت‌هاب، بعد از پوش این نسخه به main:

  • Actions → CI حالا چهار جاب دارد: تست‌های 8.2 / 8.3 / 8.4، Pint، Static Analysis (Larastan) و Coverage + Badge
  • وقتی جاب Coverage تمام شد، بج Coverage در README از حالت «inaccessible» درمی‌آید و درصد واقعی را نشان می‌دهد — و روی هر push به main خودش آپدیت می‌شود
  • یک تگ پوش کنید و Actions → Release را ببینید: اول تست‌ها اجرا می‌شوند، بعد Release با یادداشت‌های دوزبانهٔ استخراج‌شده از CHANGELOG.md در صفحهٔ Releases ظاهر می‌شود
شروع سریع (بدون تغییر)
git clone https://github.com/MSS1625/shop-php.git && cd shop-php
composer install
php artisan shop:install     # env + key + migrate + seed + link
php artisan admin:create     # ساخت مدیر (تعاملی)
php artisan serve            # → http://localhost:8000

🔑 v2.14.0 — پنل مدیریت توکن‌های API (Admin API Token Management)

Choose a tag to compare

@MSS1625 MSS1625 released this 01 Nov 07:04
a25ba7f

🔑 v2.14.0 — پنل مدیریت توکن‌های API (Admin API Token Management)

دیجی‌شاپ — فروشگاه آنلاین تجهیزات شبکه | Digi-Shop — Online network-equipment store
Laravel 12 · PHP 8.2+ · Bootstrap 5 RTL · MIT License

🇬🇧 English

🔑 New — issue, audit and revoke API tokens from the admin panel

  • The v2.12.0 REST API shipped tokens nobody could see. Now /admin/tokens lists every personal access token in one table — owner (blue «مدیر» badge for manager tokens, «کاربر حذف‌شده» for orphans), device name, issue date, last use («هرگز» badge for never-used ones) and expiry (red «منقضی‌شده» badge for dead ones)
  • Issue tokens without touching a terminal: pick a user, name the device (iPhone 15…), choose a lifetime — no expiry, 7, 30 or 90 days; the full plain token appears exactly once in a modal after the redirect (copy button included) — refresh and it's gone forever; the database still stores only the hash
  • Expiry is enforced by Sanctum itself, at authentication time: «۷ روز» really means the token dies on day 7 — and every authenticated API call stamps last_used_at, so the panel always knows when a device last called
  • Revoke in one click (with a confirm dialog): the device must log in again; the user's orders, reviews and everything else stay untouched. Tabs «همه / بدون استفاده / منقضی‌شده» (with counts) point at the cleanup candidates

📊 Numbers

  • 297 feature tests (1157 assertions) — all green; code style: Pint clean
  • Zero migrations, zero config changes — apply the zip and you're done; shop:install has nothing new to run
  • 1 new controller (Admin/TokenController); 1 new admin view (admin/tokens); 3 new routes (GET/POST /admin/tokens, DELETE /admin/tokens/{token}); 14 new tests in TokenManagementTest
Quick start (unchanged)
git clone https://github.com/MSS1625/shop-php.git && cd shop-php
composer install
php artisan shop:install     # env + key + migrate + seed + link (includes WELCOME10)
php artisan admin:create     # interactive admin user
php artisan serve            # → http://localhost:8000
Seeing it in action

Log into http://localhost:8000/admin and click «توکن‌های API» in the sidebar:

  • the table shows every token: who owns it, on which device, when it last called,
    when it expires — manager tokens carry the blue «مدیر» badge
  • open the «بدون استفاده» tab — those are the forgotten tokens; the
    «منقضی‌شده» tab shows the dead ones (red badge)
  • issue one from the form: pick a user, type iPhone 15, choose «۷ روز» and
    hit «صدور توکن» — a modal shows the full token once (copy it);
    refresh the page — it never appears again
  • call the API with it, then revoke it from the panel and try again — the
    same token is now 401, the device must log in again

Want the proof without the UI? Here is a full lifecycle through real endpoints:

# 1) the admin panel issues a token (form → PRG; token appears once in the modal HTML)
#    — do it once in the browser and copy the value, then:

# 2) the token works against the real API:
curl -s http://localhost:8000/api/v1/auth/me \
  -H "Authorization: Bearer 3|COPY_THE_TOKEN_FROM_THE_MODAL"
# → {"data":{"id":2,"name":"...", "email":"..."}}   and last_used_at gets stamped

# 3) admin revokes it (one click in /admin/tokens) — the same token is now dead:
curl -s -o /dev/null -w "%{http_code}\n" http://localhost:8000/api/v1/auth/me \
  -H "Authorization: Bearer 3|COPY_THE_TOKEN_FROM_THE_MODAL"
# → 401  (the device must log in again; the user's data is untouched)

🇮🇷 فارسی

🔑 جدید — صدور، گزارش و ابطالِ توکن‌های API از پنل مدیریت

  • API نسخهٔ v2.12.0 توکن‌هایی صادر می‌کرد که کسی نمی‌توانست ببیند. حالا /admin/tokens همهٔ توکن‌های دسترسی شخصی را در یک جدول می‌آورد — صاحب (بج آبی «مدیر» برای توکن مدیران، «کاربر حذف‌شده» برای یتیم‌ها)، نام دستگاه، تاریخ صدور، آخرین استفاده (بج «هرگز» برای هرگز استفاده‌نشده‌ها) و انقضا (بج قرمز «منقضی‌شده» برای مرده‌ها)
  • صدور توکن بدون خط فرمان: یک کاربر را انتخاب کنید، دستگاه را نام‌گذاری کنید (iPhone 15…)، عمر را تعیین کنید — بدون انقضا، ۷، ۳۰ یا ۹۰ روز؛ مقدار کامل توکن دقیقاً یک‌بار بعد از ریدایرکت در مودال ظاهر می‌شود (با دکمهٔ کپی) — رفرش کنید و برای همیشه می‌رود؛ دیتابیس همچنان فقط هش را نگه می‌دارد
  • انقضا را خود Sanctum، در لحظهٔ احراز هویت، اجرا می‌کند: «۷ روز» واقعاً یعنی روز هفتم توکن می‌میرد — و هر فراخوانی احرازشدهٔ API، last_used_at را مهر می‌زند تا پنل همیشه بداند هر دستگاه آخرین بار کِی زنگ زده
  • ابطال با یک کلیک (با دیالوگ تأیید): دستگاه باید دوباره وارد شود؛ سفارش‌ها، نظرات و بقیهٔ سوابق همان کاربر دست‌نخورده می‌مانند. تب‌های «همه / بدون استفاده / منقضی‌شده» (با شمارنده) کاندیداهای پاک‌سازی را نشانتان می‌دهند

📊 اعداد

  • ۲۹۷ تست فیچر (۱۱۵۷ ادعا) — همه سبز؛ سبک کد: Pint پاک
  • بدون مهاجرت جدید، بدون تغییر کانفیگ — زیپ را اعمال کنید و تمام؛ shop:install چیز تازه‌ای برای اجرا ندارد
  • ۱ کنترلر جدید (Admin/TokenController)؛ ۱ نمای جدید (admin/tokens)؛ ۳ مسیر جدید (GET/POST /admin/tokens و DELETE /admin/tokens/{token})؛ ۱۴ تست جدید در TokenManagementTest
راه‌اندازی سریع (بدون تغییر)
git clone https://github.com/MSS1625/shop-php.git && cd shop-php
composer install
php artisan shop:install     # env + کلید + مهاجرت + داده + لینک (شامل WELCOME10)
php artisan admin:create     # ساخت مدیر (تعاملی)
php artisan serve            # → http://localhost:8000
دیدن آن در عمل

وارد http://localhost:8000/admin شوید و در نوار کناری روی «توکن‌های API» کلیک کنید:

  • جدول همهٔ توکن‌ها را نشان می‌دهد: صاحبِ هر کدام، روی کدام دستگاه، آخرین بار کِی
    زنگ زده، کِی می‌میرد — توکن مدیران بج آبی «مدیر» دارند
  • تب «بدون استفاده» را باز کنید — این‌ها همان توکن‌های فراموش‌شده‌اند؛ تب
    «منقضی‌شده» مرده‌ها را با بج قرمز نشان می‌دهد
  • از فرم یکی صادر کنید: یک کاربر را انتخاب کنید، iPhone 15 بنویسید، «۷ روز»
    را بردارید و «صدور توکن» را بزنید — مودال مقدار کامل را یک‌بار نشان
    می‌دهد (کپی کنید)؛ صفحه را رفرش کنید — دیگر هرگز دیده نمی‌شود
  • با آن API را صدا بزنید، بعد از پنل ابطالش کنید و دوباره امتحان کنید —
    همان توکن حالا 401 است؛ دستگاه باید دوباره وارد شود

بدون UI هم می‌خواهید ببینید؟ این یک چرخهٔ کامل از دل نقاط اتصال واقعی است:

# ۱) پنل توکن را صادر می‌کند (فرم → PRG؛ توکن فقط یک‌بار در HTML مودال هست)
#    — یک‌بار در مرورگر انجامش بدهید و مقدار را کپی کنید، بعد:

# ۲) توکن در API واقعی کار می‌کند:
curl -s http://localhost:8000/api/v1/auth/me \
  -H "Authorization: Bearer 3|مقدار_توکن_را_از_مودال_کپی_کنید"
# → {"data":{"id":2,"name":"...", "email":"..."}}   و last_used_at مهر می‌خورد

# ۳) مدیر از پنل ابطالش کند (یک کلیک در /admin/tokens) — همان توکن مرده است:
curl -s -o /dev/null -w "%{http_code}\n" http://localhost:8000/api/v1/auth/me \
  -H "Authorization: Bearer 3|مقدار_توکن_را_از_مودال_کپی_کنید"
# → 401  (دستگاه باید دوباره وارد شود؛ داده‌های کاربر دست‌نخورده است)

⚖️ v2.13.0 — مقایسهٔ محصولات (Product Comparison)

Choose a tag to compare

@MSS1625 MSS1625 released this 01 Nov 06:28
067f536

⚖️ v2.13.0 — مقایسهٔ محصولات (Product Comparison)

دیجی‌شاپ — فروشگاه آنلاین تجهیزات شبکه | Digi-Shop — Online network-equipment store
Laravel 12 · PHP 8.2+ · Bootstrap 5 RTL · MIT License

🇬🇧 English

⚖️ New — Compare up to 4 products side by side

  • Customers can now line products up before they buy: a session-based compare list capped at 4 products (more columns than that and the table stops being readable) — and because it lives in the session like the cart, guests can compare without an account; no login wall between a visitor and a decision
  • One click on the scale, everywhere the product appears: the scale button sits on the corner of every product card (right under the wishlist heart), a big variant joins the price box on the product page, and the navbar gained a compare icon with a live count badge — the same instant-toggle Ajax pattern as the wishlist hearts (PRG fallback when JavaScript is off); the scale turns cyan while the product is in the list
  • /compare — the comparison table: one column per product, rows for price, category, stock, rating average + review count, answered-questions count and buy buttons; the cheapest column is highlighted green with a «بهترین قیمت» (best price) badge, so the money decision takes one glance
  • One query for the whole table, no N+1: rating aggregates come from withRating() and answered questions from a withCount subquery — the same one-query patterns the product cards and the admin dashboard already use
  • The table keeps your order: columns follow insertion order (the order you added the products, not their IDs) — preserved through the session and re-applied after the aggregate query, so the page reads like your own shortlist
  • A list that heals itself: deactivate or delete a product and it silently drops from the table while the same request rewrites the session — the navbar badge never counts a product the store no longer shows
  • Guardrails: a 5th product is refused with a friendly warning flash and, for Ajax callers, a full flag in the JSON; inactive products 404 on toggle; POST /compare/clear empties the whole list; the /compare/clear route is registered before the slug route so the two can never collide
  • Mobile-ready: the table scrolls horizontally inside table-responsive (640px floor) instead of squashing the columns; the empty state matches the wishlist page's tone and points to the catalog

📊 Numbers

  • 283 feature tests (1086 assertions) — all green; code style: Pint clean
  • Zero migrations, zero config changes — apply the zip and you're done; shop:install has nothing new to run
  • 1 new service (Compare, session-based, MAX = 4); 1 new controller (CompareController); 3 new routes (/compare, /compare/{slug}, /compare/clear); 19 new tests in CompareTest
Quick start (unchanged)
git clone https://github.com/MSS1625/shop-php.git && cd shop-php
composer install
php artisan shop:install     # env + key + migrate + seed + link (includes WELCOME10)
php artisan admin:create     # interactive admin user
php artisan serve            # → http://localhost:8000
Seeing it in action

Open http://localhost:8000/products and click the scale icon on the corner of
any product card — the scale turns cyan and the navbar badge jumps to «۱».
Add three more (try the cheapest and the most expensive in a category), then open
http://localhost:8000/compare:

  • the four products stand side by side: price, category, stock, rating, answered questions
  • the cheapest column glows green with a «بهترین قیمت» badge
  • try adding a 5th product — you get the cap warning («فهرست مقایسه پر است…»), not a broken table
  • remove one with its «حذف از فهرست» button — the column disappears without a full reload
  • everything above works logged out; compare is a session feature, like the cart

Want the state without the UI? The toggle is a plain form — any HTTP client works:

CSRF=$(curl -s -c /tmp/c.txt http://localhost:8000/products \
 | grep -oP 'csrf-token" content="\K[^"]+')

# add a product to the compare list (Ajax branch returns JSON):
curl -s -b /tmp/c.txt -X POST http://localhost:8000/compare/fiber-optic-cable \
  -H "X-Requested-With: XMLHttpRequest" -H "Accept: application/json" \
  -H "X-CSRF-TOKEN: $CSRF"
# → {"success":true,"inCompare":true,"full":false,"count":1,"message":"«کابل فیبر نوری» به فهرست مقایسه اضافه شد."}

curl -s -b /tmp/c.txt http://localhost:8000/compare | grep -c ds-compare-table
# → 1  (the table renders for this session)

curl -s -b /tmp/c.txt -X POST http://localhost:8000/compare/clear \
  -H "X-CSRF-TOKEN: $CSRF" -o /dev/null -w "%{http_code}\n"
# → 302 (list emptied, PRG back to /compare)

🇮🇷 فارسی

⚖️ جدید — مقایسهٔ حداکثر ۴ محصول، کنار-به-کنار

  • مشتری حالا می‌تواند قبل از خرید، محصولات را کنار هم بچیند: فهرست مقایسهٔ مبتنی بر سشن با سقف ۴ محصول (بیشتر از آن، جدول دیگر خوانا نمی‌ماند) — و چون مثل سبد خرید روی سشن زندگی می‌کند، مهمان هم بدون حساب کاربری مقایسه می‌کند؛ بین بازدیدکننده و تصمیمش دیواری نیست
  • یک کلیک روی ترازو، همه‌جایی که محصول هست: دکمهٔ ترازو گوشهٔ کارت هر محصول است (درست زیر قلب علاقه‌مندی)، نسخهٔ بزرگش به ردیف قیمتِ صفحهٔ محصول می‌پیوندد و ناوبار آیکون مقایسه با شمارندهٔ زنده گرفت — همان الگوی Ajax یک‌کلیکی قلب‌های علاقه‌مندی (بدون جاوااسکریپت همان فرم عادی سابمیت می‌شود)؛ ترازو در فهرست بودن محصول را با رنگ فیروزه‌ای نشان می‌دهد
  • /compare — جدول مقایسه: یک ستون برای هر محصول و ردیف‌های قیمت، دسته‌بندی، موجودی، میانگین امتیاز + تعداد نظر، پرسش‌های پاسخ‌داده و دکمه‌های خرید؛ ارزان‌ترین ستون سبز می‌شود با بج «بهترین قیمت» تا تصمیم مالی با یک نگاه گرفته شود
  • یک کوئری برای کل جدول، بدون N+1: میانگین امتیاز از withRating() و پرسش‌های پاسخ‌داده از subquery یک withCount می‌آیند — همان الگوهای یک-کوئری کارت‌های محصول و داشبورد ادمین
  • جدول ترتیب شما را نگه می‌دارد: ستون‌ها به ترتیب افزودن می‌آیند (ترتیبی که محصول‌ها را اضافه کردید، نه ترتیب شناسه‌ها) — هم در سشن و هم بعد از کوئری تجمیعی؛ صفحه مثل فهرست انتخابی خودتان خوانده می‌شود
  • فهرستی که خودش را ترمیم می‌کند: محصولی را غیرفعال یا حذف کنید — بی‌سروصدا از جدول می‌افتد و همان درخواست، سشن را بازمی‌نویسد؛ شمارندهٔ ناوبار هرگز محصولی را نمی‌شمارد که فروشگاه دیگر نشان نمی‌دهد
  • حفاظت‌ها: محصول پنجم با فلش هشداری رد می‌شود و برای Ajax با فیلد full در JSON؛ محصول غیرفعال روی toggle ارور 404 می‌گیرد؛ POST /compare/clear کل فهرست را خالی می‌کند؛ مسیر /compare/clear قبل از مسیر اسلاگ ثبت شده تا هرگز تداخل نکنند
  • آمادهٔ موبایل: جدول داخل table-responsive (کف 640px) به‌طور افقی اسکرول می‌خورد به‌جای اینکه ستون‌ها له شوند؛ حالت خالی هم‌لحن صفحهٔ علاقه‌مندی‌هاست و به ویترین می‌رساند

📊 اعداد

  • ۲۸۳ تست فیچر (۱۰۸۶ ادعا) — همه سبز؛ سبک کد: Pint پاک
  • بدون مهاجرت جدید، بدون تغییر کانفیگ — زیپ را اعمال کنید و تمام؛ shop:install چیز تازه‌ای برای اجرا ندارد
  • ۱ سرویس جدید (Compare، سشن‌محور، MAX = 4)؛ ۱ کنترلر جدید (CompareController)؛ ۳ مسیر جدید (/compare، /compare/{slug}، /compare/clear)؛ ۱۹ تست جدید در CompareTest
راه‌اندازی سریع (بدون تغییر)
git clone https://github.com/MSS1625/shop-php.git && cd shop-php
composer install
php artisan shop:install     # env + کلید + مهاجرت + داده + لینک (شامل WELCOME10)
php artisan admin:create     # ساخت مدیر (تعاملی)
php artisan serve            # → http://localhost:8000
دیدن آن در عمل

http://localhost:8000/products را باز کنید و روی آیکون ترازو گوشهٔ هر کارت محصول
کلیک کنید — ترازو فیروزه‌ای می‌شود و شمارندهٔ ناوبار می‌پرد به «۱». سه محصول دیگر
باید امتحان کنید (مثلاً ارزان‌ترین و گران‌ترین یک دسته) و بعد
http://localhost:8000/compare را باز کنید:

  • چهار محصول کنار هم می‌ایستند: قیمت، دسته، موجودی، امتیاز، پرسش‌های پاسخ‌داده
  • ارزان‌ترین ستون سبز می‌درخشد با بج «بهترین قیمت»
  • محصول پنجم را هم امتحان کنید — پیام سقف می‌گیرید («فهرست مقایسه پر است…»)، نه جدولِ شکسته
  • با دکمهٔ «حذف از فهرست» یکی را بردارید — ستون بدون رفرش کامل می‌رود
  • همهٔ این‌ها بدون ورود کار می‌کند؛ مقایسه مثل سبد، فیچر سشنی است

بدون UI هم می‌خواهید ببینید؟ toggle یک فرم ساده است — هر کلاینت HTTP جواب می‌دهد:

CSRF=$(curl -s -c /tmp/c.txt http://localhost:8000/products \
 | grep -oP 'csrf-token" content="\K[^"]+')

# افزودن محصول به فهرست (شاخهٔ Ajax پاسخ JSON می‌دهد):
curl -s -b /tmp/c.txt -X POST http://localhost:8000/compare/fiber-optic-cable \
  -H "X-Requested-With: XMLHttpRequest" -H "Accept: application/json" \
  -H "X-CSRF-TOKEN: $CSRF"
# → {"success":true,"inCompare":true,"full":false,"count":1,"message":"«کابل فیبر نوری» به فهرست مقایسه اضافه شد."}

curl -s -b /tmp/c.txt http://localhost:8000/compare | grep -c ds-compare-table
# → 1  (جدول برای همین سشن رندر می‌شود)

curl -s -b /tmp/c.txt -X POST http://localhost:8000/compare/clear \
  -H "X-CSRF-TOKEN: $CSRF" -o /dev/null -w "%{http_code}\n"
# → 302 (فهرست خالی شد، PRG به /compare)

📡 v2.12.0 — REST API v1 (Sanctum, Mobile-Ready)

Choose a tag to compare

@MSS1625 MSS1625 released this 31 Oct 05:54
de26b3f

📡 v2.12.0 — REST API v1 (Sanctum, Mobile-Ready)

دیجی‌شاپ — فروشگاه آنلاین تجهیزات شبکه | Digi-Shop — Online network-equipment store
Laravel 12 · PHP 8.2+ · Sanctum 4 · Bootstrap 5 RTL · MIT License

🇬🇧 English

📡 New — REST API v1, authenticated with Sanctum tokens

  • The whole store is now available to a mobile app or any external client: 18 endpoints under /api/v1 — auth, catalog, reviews, questions, wishlist and orders — authenticated with Laravel Sanctum personal access tokens (Authorization: Bearer …); the web shop keeps its session flow untouched
  • Token auth that behaves like a phone: POST /auth/login names the token after your device_name; POST /auth/logout revokes only the current device's token — sign out on the phone, stay logged in on the tablet; login errors are deliberately generic so the API never reveals whether an email exists; tokens are stored hashed
  • The catalog with the same rules as the web: GET /products runs the exact same filter request as the storefront — search, category, price ranges typed in Persian digits (۵۰۰٬۰۰۰), in-stock filter and best-selling sort (withSum); GET /products/{slug} fills an entire product screen in one call: gallery, rating summary + 5→1 breakdown, first page of approved reviews with verified-buyer badges (one query, no N+1), answered questions and 4 related products
  • Orders placed straight from the app: POST /orders takes items: [{slug, quantity}], customer details, payment method and an optional coupon_code — everything runs through the new shared PlaceOrder service (the web checkout was refactored onto it too): final stock re-validation, row-locked coupon redemption, price snapshots, stock decrement and mail/SMS notifications behave identically on both surfaces; ZarinPal orders answer with a payment_url for WebView hand-off, and every response carries raw values (status, total) plus ready-made Persian labels (status_label, total_fa)
  • The social layer, readable and writable: paginated GET endpoints for reviews and questions (approved/answered only, verified badges included); POST endpoints enforce the web rules — one review per user/product, pending moderation on both, throttle:5,1
  • Idempotent wishlist: POST / DELETE /wishlist/{slug} always return the final state — a retry after a flaky connection never errors; product responses carry is_in_wishlist for logged-in tokens (one query per page)
  • Guardrails: 60 req/min per token (throttle:api), 10/min on login/register, standard 422 validation shape, 409 for stock conflicts with the Persian message, the same [a-z0-9-]+ slug constraint as the web
  • Housekeeping: laravel/sanctum ^4.3 (one migration — personal_access_tokens — applied automatically by shop:install); symfony/mime bumped past CVE-2026-45070 (composer audit clean); fa_price() now uses the Persian thousands separator «٬» — ۲٬۰۰۰٬۰۰۰ everywhere
  • 48 new tests across 5 API suites: auth flows, token revocation, filters + Persian-digit parsing, one-call detail shape, verified badges, wishlist idempotency, ownership isolation, direct orders with coupon math, 409 stock conflicts, restock on cancel, email delivery

📊 Numbers

  • 264 feature tests (990 assertions) — all green; code style: Pint clean
  • One new migration (personal_access_tokens) — php artisan shop:install or plain migrate handles it; zero config changes
  • 18 new API endpoints under /api/v1; 1 new shared service (PlaceOrder); 7 API resources; 5 new feature-test suites
Quick start (unchanged)
git clone https://github.com/MSS1625/shop-php.git && cd shop-php
composer install
php artisan shop:install     # env + key + migrate + seed + link (includes WELCOME10)
php artisan admin:create     # interactive admin user
php artisan serve            # → http://localhost:8000
Seeing it in action
# Log in from your "app" and grab a token:
curl -s -X POST http://localhost:8000/api/v1/auth/login \
  -H "Accept: application/json" -H "Content-Type: application/json" \
  -d '{"email":"you@example.com","password":"secret","device_name":"Pixel 8"}'
# → {"message":"خوش آمدید!","data":{…},"token":"1|abc…"}

# Browse the catalog — Persian digits work in price filters:
curl -s "http://localhost:8000/api/v1/products?sort=popular&min=۵۰۰٬۰۰۰" \
  -H "Accept: application/json"

# Place an order straight from the app, with a coupon:
curl -s -X POST http://localhost:8000/api/v1/orders \
  -H "Authorization: Bearer $TOKEN" -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  -d '{"items":[{"slug":"network-switch","quantity":1}],
       "customer_name":"سارا محمدی","customer_phone":"09123456789",
       "customer_address":"تهران، خیابان ولیعصر، پلاک ۱۲",
       "payment_method":"cod","coupon_code":"WELCOME10"}'
# → 201 + order with status_label, total_fa and payment_url when online

# Sign out on just this device — the tablet keeps working:
curl -s -X POST http://localhost:8000/api/v1/auth/logout \
  -H "Authorization: Bearer $TOKEN" -H "Accept: application/json"

🇮🇷 فارسی

📡 جدید — REST API نسخهٔ ۱ با توکن Sanctum

  • کل فروشگاه حالا در دسترس اپلیکیشن موبایل یا هر کلاینت بیرونی است: ۱۸ نقطهٔ اتصال زیر /api/v1 — احراز هویت، کاتالوگ، نظرات، پرسش‌ها، علاقه‌مندی‌ها و سفارش‌ها — با توکن دسترسی شخصی Sanctum (Authorization: Bearer …)؛ جریان سشن وب دست‌نخورده ماند
  • توکنی که مثل گوشی رفتار می‌کند: POST /auth/login توکن را به نام device_name شما ثبت می‌کند؛ POST /auth/logout فقط توکن همین دستگاه را باطل می‌کند — از گوشی خارج شوید، تبلت لاگین می‌ماند؛ پیام خطای ورود عمداً کلی است تا API لو ندهد ایمیل وجود دارد یا نه؛ توکن‌ها هش‌شده ذخیره می‌شوند
  • کاتالوگ با همان قواعد وب: GET /products همان فیلترهای ویترین را اجرا می‌کند — جستجو، دسته، بازهٔ قیمت با ارقام فارسی (۵۰۰٬۰۰۰)، فیلتر موجودی و پرفروش‌ترین (withSum)؛ GET /products/{slug} کل صفحهٔ محصول را در یک فراخوانی پر می‌کند: گالری، خلاصهٔ امتیاز + تفکیک ۵→۱، اولین صفحهٔ نظرات تأییدشده با نشان خریدار تأییدشده (یک کوئری، بدون N+1)، پرسش‌های پاسخ‌داده و ۴ محصول مشابه
  • سفارش مستقیم از اپ: POST /orders با items، اطلاعات مشتری، روش پرداخت و coupon_code اختیاری — همه‌چیز از سرویس مشترک جدید PlaceOrder می‌گذرد (فرم خرید وب هم روی آن بازآرایی شد): اعتبارسنجی نهایی موجودی، نقد کردن کوپن با قفل ردیف، اسنپ‌شات قیمت، کاهش انبار و اطلاع‌رسانی ایمیل/پیامک روی هر دو سطح یکی است؛ سفارش زرین‌پال payment_url برای WebView برمی‌گرداند و هر پاسخ هم مقدار خام دارد (status, total) هم برچسب فارسی آماده (status_label, total_fa)
  • لایهٔ اجتماعی، خواندنی و نوشتنی: GETهای صفحه‌بندی‌شدهٔ نظرات و پرسش‌ها (فقط تأییدشده/پاسخ‌داده، با نشان خریدار)؛ POSTها همان قواعد وب را دارند — یک نظر برای هر کاربر/محصول، شروع در انتظار تأیید، throttle:5,1
  • علاقه‌مندی idempotent: POST / DELETE /wishlist/{slug} همیشه وضعیت نهایی را برمی‌گردانند — تلاش دوباره بعد از قطع اینترنت خطا نمی‌دهد؛ پاسخ محصولات برای توکن لاگین‌شده is_in_wishlist دارد (یک کوئری برای کل صفحه)
  • حفاظت‌ها: ۶۰ درخواست در دقیقه برای هر توکن (throttle:api)، ۱۰ در دقیقه برای ورود/ثبت‌نام، شکل استاندارد 422 برای اعتبارسنجی، 409 برای تعارض موجودی با پیام فارسی، همان قید اسلاگ [a-z0-9-]+ وب
  • کارهای جانبی: laravel/sanctum نسخهٔ ^4.3 (یک مایگریشن — personal_access_tokens — که shop:install خودش اعمال می‌کند)؛ symfony/mime از CVE-2026-45070 عبور داده شد (composer audit پاک)؛ fa_price() حالا جداکنندهٔ هزارگان فارسی «٬» می‌گذارد — ۲٬۰۰۰٬۰۰۰ همه‌جا
  • ۴۸ تست جدید در ۵ مجموعهٔ API: جریان‌های احراز هویت، باطل‌کردن توکن، فیلترها + پارس ارقام فارسی، شکل پاسخ یک‌فراخوانی، نشان‌های خریدار، idempotency علاقه‌مندی، جداسازی مالکیت، سفارش مستقیم با محاسبهٔ کوپن، تعارض 409، بازگشت موجودی هنگام لغو، ارسال ایمیل

📊 اعداد

  • ۲۶۴ تست فیچر (۹۹۰ ادعا) — همه سبز؛ سبک کد: Pint پاک
  • یک مهاجرت جدید (personal_access_tokens) — با php artisan shop:install یا migrate ساده اعمال می‌شود؛ بدون تغییر کانفیگ
  • ۱۸ نقطهٔ اتصال جدید زیر /api/v1؛ ۱ سرویس مشترک جدید (PlaceOrder)؛ ۷ منبع API؛ ۵ مجموعهٔ تست جدید
راه‌اندازی سریع (بدون تغییر)
git clone https://github.com/MSS1625/shop-php.git && cd shop-php
composer install
php artisan shop:install     # env + کلید + مهاجرت + داده + لینک (شامل WELCOME10)
php artisan admin:create     # ساخت مدیر (تعاملی)
php artisan serve            # → http://localhost:8000
دیدن آن در عمل
# از «اپ» وارد شوید و توکن بگیرید:
curl -s -X POST http://localhost:8000/api/v1/auth/login \
  -H "Accept: application/json" -H "Content-Type: application/json" \
  -d '{"email":"you@example.com","password":"secret","device_name":"Pixel 8"}'
# → {"message":"خوش آمدید!","data":{…},"token":"1|abc…"}

# کاتالوگ را ببینید — ارقام فارسی در فیلتر قیمت کار می‌کند:
curl -s "http://localhost:8000/api/v1/products?sort=popular&min=۵۰۰٬۰۰۰" \
  -H "Accept: application/json"

# مستقیم از اپ سفارش ثبت کنید، با کد تخفیف:
curl -s -X POST http://localhost:8000/api/v1/orders \
  -H "Authorization: Bearer $TOKEN" -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  -d '{"items":[{"slug":"network-switch","quantity":1}],
       "customer_name":"سارا محمدی","customer_phone":"09123456789",
       "customer_address":"تهران، خیابان ولیعصر، پلاک ۱۲",
       "payment_method":"cod","coupon_code":"WELCOME10"}'
# → 201 + سفارش با status_label و total_fa و payment_url برای سفارش آنلاین

# فقط از همین دستگاه خارج شوید — تبلت کارش را می‌کند:
curl -s -X POST http://localhost:8000/api/v1/auth/logout \
  -H "Authorization: Bearer $TOKEN" -H "Accept: application/json"

</...

Read more

❓ v2.11.0 — Product Questions & Answers

Choose a tag to compare

@MSS1625 MSS1625 released this 30 Oct 13:38
de26b3f

❓ v2.11.0 — Product Questions & Answers

دیجی‌شاپ — فروشگاه آنلاین تجهیزات شبکه | Digi-Shop — Online network-equipment store
Laravel 12 · PHP 8.2+ · Bootstrap 5 RTL · MIT License

🇬🇧 English

❓ New — Product Questions & Answers

  • Customers can now ask a question right on the product page — a new «پرسش و پاسخ» section sits below the reviews; questions start as «در انتظار پاسخ» and stay private until answered, so nothing unanswered ever reaches the storefront
  • The store answers with one voice: the new /admin/questions panel lists every question with status tabs and counts (همه / در انتظار پاسخ / پاسخ داده شد / ردشده) — and the answer box sits right inside the table row, so answering is one textarea + one click, no page jumps
  • Publishing and notifying in a single step: the moment an admin answers, the question goes public and the asker receives a «پاسخ پرسش شما» email — built on the same mail design as the order notifications, question and answer side by side with a link back to the product; editing an answer later is silent, and an SMTP failure never blocks the answering itself (logged, not thrown)
  • The public Q&A cards carry the same «خریدار تأییدشده» (verified buyer) badge as reviews — computed for the whole page with a single query (no N+1), reusing the same cancelled-excluded rule
  • Unlike reviews, one user may ask several questions about the same product — real buyers ask follow-ups; anti-spam stays intact via auth + throttle:5,1 plus admin reject/delete
  • The admin sidebar gained a «پرسش‌ها» link with a pending-count badge next to «نظرات» — mirroring the orders and reviews workflows
  • 35 new tests (QuestionTest): submission guards and validation, visibility rules, pagination, verified badges, email lifecycle (sent once, silent on edit, never breaking on failure), moderation flows, access guards and cascade integrity

📊 Numbers

  • 216 feature tests (778 assertions) — 35 new Q&A tests: guest redirect, body required/min-length, pending start + trim, multiple questions per user, inactive 404, rate limit 429, pending/rejected invisible, answered visible + count heading, 5-per-page pagination (newest first), verified badge (real buyer / non-buyer / cancelled-order asker), guest login prompt vs authed form, empty state, admin access guards, tabs + counts, answer flow + publishing, answer validation, email sent / not on reject / not resent on edit / never breaking, first answered_at preserved on edit, rejected-question revival, reject, delete, customer moderation 403, sidebar badge, product/user cascade, excerpt
  • Code style: Pint clean
  • One new migration (questions) — php artisan shop:install or plain migrate handles it; zero config changes
Quick start (unchanged)
git clone https://github.com/MSS1625/shop-php.git && cd shop-php
composer install
php artisan shop:install     # env + key + migrate + seed + link (includes WELCOME10)
php artisan admin:create     # interactive admin user
php artisan serve            # → http://localhost:8000
Seeing it in action
# Register an account, open any product page:
# → scroll below the reviews — the «پرسش و پاسخ» section:
#   type «گارانتی این محصول چند سال است؟» and hit «ثبت پرسش».
# → Log in as admin → /admin/questions: the question waits in
#   «در انتظار پاسخ» with a yellow sidebar badge. Type the answer
#   right inside the row and click «ثبت پاسخ».
# → Back on the product page the question + «پاسخ دیجی‌شاپ» are now
#   public — and the asker's inbox has the «پاسخ پرسش شما» email.
# → Edit the answer in admin: the page updates, but no second
#   email goes out — edits are silent.

🇮🇷 فارسی

❓ جدید — پرسش و پاسخ محصولات

  • مشتری‌ها حالا می‌توانند مستقیم در صفحهٔ محصول سؤال بپرسند — بخش جدید «پرسش و پاسخ» زیر نظرات قرار دارد؛ پرسش‌ها «در انتظار پاسخ» شروع می‌شوند و تا پاسخ‌گرفتن خصوصی می‌مانند؛ هیچ پرسش بی‌پاسخی به فروشگاه نمی‌رسد
  • فروشگاه با یک صدا جواب می‌دهد: پنل جدید /admin/questions همهٔ پرسش‌ها را با تب‌های وضعیت و شمارنده نشان می‌دهد (همه / در انتظار پاسخ / پاسخ داده شد / ردشده) — و جعبهٔ پاسخ داخل خود ردیف جدول است؛ پاسخ‌دادن یعنی یک textarea و یک کلیک، بدون پرش صفحه
  • انتشار و خبررسانی در یک قدم: به محض پاسخ مدیر، پرسش عمومی می‌شود و پرسنده ایمیل «پاسخ پرسش شما» را می‌گیرد — با همان طراحی ایمیل‌های سفارش، پرسش و پاسخ کنار هم و لینک برگشت به محصول؛ ویرایش بعدی بی‌صداست و خطای SMTP هرگز پاسخ‌دادن را نمی‌شکند (لاگ می‌شود، نه خطا)
  • کارت‌های عمومی پرسش و پاسخ همان نشان «خریدار تأییدشده» نظرات را دارند — با یک کوئری برای کل صفحه (بدون N+1) و همان قانونِ عدم‌شمول سفارش‌های لغوشده
  • برخلاف نظرات، یک کاربر می‌تواند چند پرسش دربارهٔ همان محصول بپرسد — خریدارهای واقعی سؤال بعدی هم دارند؛ ضداسپم با auth + throttle:5,1 و رد/حذف مدیر سر جایش است
  • نوار کناری پنل هم لینک «پرسش‌ها» با نشان زرد در انتظار کنار «نظرات» گرفت — قرینهٔ جریان سفارش‌ها و نظرات
  • ۳۵ تست جدید (QuestionTest): گاردهای ثبت و اعتبارسنجی، قوانین نمایش، صفحه‌بندی، نشان‌های خریدار، چرخهٔ ایمیل (یک‌بار ارسال، بی‌صدا در ویرایش، بی‌اثر در خطا)، جریان‌های نظارت، گاردهای دسترسی و یکپارچگی آبشاری

📊 اعداد

  • ۲۱۶ تست فیچر (۷۷۸ ادعا) — ۳۵ تست جدید پرسش و پاسخ: ریدایرکت مهمان، الزامی/حداقل متن، شروع در انتظار + trim، چند پرسش برای هر کاربر، ۴۰۴ غیرفعال، محدودیت نرخ، نامرئی‌بودن در انتظار/ردشده، دیده‌شدن پاسخ + شمارنده، صفحه‌بندی ۵تایی (جدیدترین اول)، نشان خریدار (واقعی / غیرخریدار / لغوشده)، راهنمای ورود در برابر فرم، حالت خالی، گاردهای ادمین، تب‌ها، جریان پاسخ + انتشار، اعتبارسنجی پاسخ، ایمیل (ارسال / عدم ارسال در رد / عدم تکرار در ویرایش / بی‌شکستن در خطا)، حفظ تاریخ پاسخ اول، احیای ردشده، رد، حذف، 403 مشتری، نشان نوار کناری، cascade محصول/کاربر، برش متن
  • سبک کد: Pint پاک
  • یک مهاجرت جدید (questions) — با php artisan shop:install یا migrate ساده اعمال می‌شود؛ بدون تغییر کانفیگ
راه‌اندازی سریع (بدون تغییر)
git clone https://github.com/MSS1625/shop-php.git && cd shop-php
composer install
php artisan shop:install     # env + کلید + مهاجرت + داده + لینک (شامل WELCOME10)
php artisan admin:create     # ساخت مدیر (تعاملی)
php artisan serve            # → http://localhost:8000
دیدن آن در عمل
# ثبت‌نام کنید و صفحهٔ یک محصول را باز کنید:
# ← زیر نظرات، بخش «پرسش و پاسخ» را پیدا کنید:
#   «گارانتی این محصول چند سال است؟» را بنویسید و «ثبت پرسش» را بزنید.
# ← به‌عنوان مدیر وارد شوید → /admin/questions: پرسش در تب
#   «در انتظار پاسخ» با نشان زرد نوار کناری منتظر است. پاسخ را
#   داخل خود ردیف بنویسید و «ثبت پاسخ» را بزنید.
# ← در صفحهٔ محصول، پرسش + «پاسخ دیجی‌شاپ» حالا عمومی است —
#   و صندوق پرسنده ایمیل «پاسخ پرسش شما» را گرفته.
# ← پاسخ را در پنل ویرایش کنید: صفحه به‌روز می‌شود ولی ایمیل
#   دوم نمی‌رود — ویرایش بی‌صداست.

❤️ v2.10.0 — Customer Wishlist

Choose a tag to compare

@MSS1625 MSS1625 released this 30 Oct 13:37
de26b3f

❤️ v2.10.0 — Customer Wishlist

دیجی‌شاپ — فروشگاه آنلاین تجهیزات شبکه | Digi-Shop — Online network-equipment store
Laravel 12 · PHP 8.2+ · Bootstrap 5 RTL · MIT License

🇬🇧 English

❤️ New — Customer Wishlist

  • Customers can now save products with one click on the heart — the button sits on every product card and next to add-to-cart on the product page; a second click removes it (toggle), and with JavaScript the heart flips in place with a toast, no reload
  • Progressive enhancement, not JavaScript-dependence: the heart is a real POST form with a Post/Redirect/Get fallback, so it works even with scripting disabled — the same philosophy as the rest of the storefront
  • New /wishlist page: saved products on the familiar product cards, newest saved first, 12 per page — linked from the navbar heart (with a live count badge) and the account dropdown; a friendly empty state guides new users to the shop
  • Hearts stay in sync across the whole storefront (listings, product page, related products) via a new Wishlist singleton service — the exact pattern the cart already uses: one memoized query per request, no per-card queries, no N+1
  • One row per user per product, enforced by the database (unique constraint + idempotent toggle) — double-clicks and double-tab submits can't create duplicates
  • Only active products count: something the admin deactivated quietly disappears from the wishlist and the badge; deleting a product cascades its wishlist rows away
  • Guests clicking a heart land on the login page with a hint — never a dead button
  • 19 new tests (WishlistTest) covering access, toggle flows, Ajax JSON, inactive guards, ordering, pagination, navbar badge states and cascade integrity

📊 Numbers

  • 181 feature tests (~690 assertions) — 19 new wishlist tests: guest access, add/remove toggle, JSON responses, inactive-product guard, newest-first ordering, own-items-only, inactive hiding, empty state, 12-per-page pagination, navbar badge (Persian numeral, active-only, hidden for guests), solid/outline heart states, product-page form, account dropdown link, product-deletion cascade
  • Code style: Pint clean
  • One new migration (wishlists) — php artisan shop:install or plain migrate handles it; zero config changes
Quick start (unchanged)
git clone https://github.com/MSS1625/shop-php.git && cd shop-php
composer install
php artisan shop:install     # env + key + migrate + seed + link (includes WELCOME10)
php artisan admin:create     # interactive admin user
php artisan serve            # → http://localhost:8000
Seeing it in action
# Register an account, browse the products listing:
# → click the heart on a card — it fills in red, the navbar heart
#   gains a count badge (۱) and a toast confirms «به علاقه‌مندی‌ها اضافه شد».
# → click the navbar heart: the /wishlist page lists your saved
#   products, newest first — click the heart again to remove one,
#   the card fades and the badge counts down.
# → Log out: hearts become login links — no dead buttons for guests.

🇮🇷 فارسی

❤️ جدید — علاقه‌مندی‌های مشتری

  • مشتری‌ها حالا می‌توانند محصولات را با یک کلیک روی قلب ذخیره کنند — دکمه روی همهٔ کارت‌های محصول و کنار «افزودن به سبد» در صفحهٔ جزئیات هست؛ کلیک دوم همان محصول را حذف می‌کند (toggle) و با جاوااسکریپت قلب همان‌جا می‌چرخد، بدون رفرش
  • بهبود تدریجی، نه وابستگی به جاوااسکریپت: قلب یک فرم POST واقعی با جریان Post/Redirect/Get است و حتی با خاموش‌بودن اسکریپت‌ها کار می‌کند — همان فلسفهٔ بقیهٔ فروشگاه
  • صفحهٔ جدید /wishlist: محصولات ذخیره‌شده روی همان کارت‌های آشنا، جدیدترین ذخیره اول، ۱۲تا در صفحه — از قلب ناوبار (با شمارندهٔ زنده) و منوی حساب در دسترس؛ حالت خالیِ دوستانه کاربر تازه‌وارد را به فروشگاه می‌فرستد
  • قلب‌ها در کل فروشگاه (لیست، صفحهٔ جزئیات، محصولات مشابه) همگام می‌مانند از طریق سرویس singleton جدید Wishlist — دقیقاً همان الگویی که سبد خرید از قبل دارد: یک کوئری memoize در هر درخواست، بدون کوئری برای هر کارت، بدون N+1
  • یک ردیف برای هر کاربر/محصول، با پشتیبان دیتابیس (قید یکتا + toggle پیش‌فرض) — دابل‌کلیک و سابمیت همزمان از دو تب تکراری نمی‌سازد
  • فقط محصولات فعال حساب می‌شوند: محصولی که مدیر غیرفعال کند بی‌صدا از فهرست و شمارنده کنار می‌رود؛ حذف محصول هم ردیف‌های علاقه‌مندی‌اش را آبشاری پاک می‌کند
  • مهمان‌ها با کلیک روی قلب با یک راهنما به صفحهٔ ورود می‌رسند — هرگز دکمهٔ مرده نمی‌بینند
  • ۱۹ تست جدید (WishlistTest): دسترسی، جریان‌های toggle، پاسخ Ajax، گاردهای غیرفعال، ترتیب، صفحه‌بندی، وضعیت‌های شمارندهٔ ناوبار و یکپارچگی آبشاری

📊 اعداد

  • ۱۸۱ تست فیچر (۶۸۵ ادعا) — ۱۹ تست جدید علاقه‌مندی: دسترسی مهمان، افزودن/حذف، پاسخ JSON، محافظ محصول غیرفعال، ترتیب جدیدترین‌اول، فقط-آیتم‌های-خود، مخفی‌شدن غیرفعال‌ها، حالت خالی، صفحه‌بندی ۱۲تایی، شمارندهٔ ناوبار (عدد فارسی، فعال-فقط، مخفی برای مهمان)، وضعیت پر/خالی قلب، فرم صفحهٔ جزئیات، لینک منوی حساب، cascade حذف
  • سبک کد: Pint پاک
  • یک مهاجرت جدید (wishlists) — با php artisan shop:install یا migrate ساده اعمال می‌شود؛ بدون تغییر کانفیگ
راه‌اندازی سریع (بدون تغییر)
git clone https://github.com/MSS1625/shop-php.git && cd shop-php
composer install
php artisan shop:install     # env + کلید + مهاجرت + داده + لینک (شامل WELCOME10)
php artisan admin:create     # ساخت مدیر (تعاملی)
php artisan serve            # → http://localhost:8000
دیدن آن در عمل
# ثبت‌نام کنید و لیست محصولات را باز کنید:
# ← روی قلب یک کارت کلیک کنید — قرمز پر می‌شود، قلب ناوبار
#   شمارندهٔ (۱) می‌گیرد و پیام «به علاقه‌مندی‌ها اضافه شد» می‌آید.
# ← روی قلب ناوبار کلیک کنید: صفحهٔ /wishlist محصولات ذخیره‌شده را
#   جدیدترین‌اول نشان می‌دهد — قلب را دوباره بزنید تا حذف شود،
#   کارت محو می‌شود و شمارنده کم می‌شود.
# ← خارج شوید: قلب‌ها لینک ورود می‌شوند — مهمان دکمهٔ مرده نمی‌بیند.

⭐ v2.9.0 — Product Reviews & Ratings

Choose a tag to compare

@MSS1625 MSS1625 released this 24 Oct 07:52
4581e8a

⭐ v2.9.0 — Product Reviews & Ratings

دیجی‌شاپ — فروشگاه آنلاین تجهیزات شبکه | Digi-Shop — Online network-equipment store
Laravel 12 · PHP 8.2+ · Bootstrap 5 RTL · MIT License

🇬🇧 English

⭐ New — Product Reviews & Ratings

  • Customers can now rate products 1–5 stars and write a review on the product page — every review starts as pending and only goes public after an admin approves it; nothing unmoderated ever reaches the storefront
  • «خریدار تأییدشده» (verified buyer) badge — a review whose author actually bought that product in a non-cancelled order gets the green badge; computed with a single query for the whole page, following the same cancelled-excluded rule as the dashboard's sales figures
  • The product page gained a rating summary: big Persian-decimal average («۴٫۳»), star row and a 5→1 breakdown chart with progress bars — plus a stars+average link under the product title that jumps straight to the reviews
  • Product cards show the rating now (star, average, count) across the home page, listing and related products — one withRating() scope, two subqueries, the same pattern as the best-selling sort
  • Sensible rules baked in: one review per user per product (DB unique + friendly error + race-safe catch), the form becomes a status box after reviewing, guests get a login prompt, auth + throttle:5,1 keep the spam out
  • A polished RTL star picker with hover-to-preview, and validation in a dedicated ReviewRequest (rating 1–5, comment 5–2000, Persian messages)
  • New admin reviews page at /admin/reviews: status tabs with counts, one-click approve / reject / delete, product links, and a pending badge in the sidebar — mirroring the orders workflow

📊 Numbers

  • 162 feature tests (632 assertions) — 29 new review tests: access guards, validation, pending-until-approved flow, rejection, one-review rule, rate limit, average/count/breakdown rendering, approved-only aggregates, verified-buyer badge (real / non-buyer / cancelled order), guest prompt, own-review status box, admin moderation flows, sidebar badge, product-deletion cascade
  • Code style: Pint clean across 110 files
  • One new migration (reviews) — php artisan shop:install or plain migrate handles it; zero config changes
Quick start (unchanged)
git clone https://github.com/MSS1625/shop-php.git && cd shop-php
composer install
php artisan shop:install     # env + key + migrate + seed + link (includes WELCOME10)
php artisan admin:create     # interactive admin user
php artisan serve            # → http://localhost:8000
Seeing it in action
# Register an account, open any product and scroll to «نظرات خریداران»:
# → hover the star picker, pick 5, write a comment, «ثبت نظر» —
#   you get the «نظر شما ثبت شد و پس از تأیید…» message and a status box.
# → Log in as admin → نظرات: the sidebar shows a pending badge (۲);
#   the tabs read در انتظار تأیید (۲) / تأییدشده (۶) — hit the ✓ on a
#   pending review: it instantly appears on the product page, badge
#   «خریدار تأییدشده» included for real buyers.
# → The products listing now shows «★ ۴٫۳ (۴ نظر)» on rated cards.

🇮🇷 فارسی

⭐ جدید — نظرات و امتیازدهی محصولات

  • مشتری‌ها حالا روی صفحهٔ محصول می‌توانند از ۱ تا ۵ ستاره امتیاز بدهند و نظر بنویسند — هر نظر با وضعیت «در انتظار تأیید» شروع می‌شود و فقط بعد از تأیید مدیر عمومی می‌شود؛ هیچ نظر بررسی‌نشده‌ای راه ویترین پیدا نمی‌کند
  • نشان «خریدار تأییدشده» — نویسنده‌ای که واقعاً همان محصول را در سفارشی غیرلغوشده خریده، نشان سبز می‌گیرد؛ با یک کوئری برای کل صفحه و با همان قاعدهٔ حذف سفارش‌های لغوشدهٔ داشبورد
  • صفحهٔ محصول به خلاصهٔ امتیازها مجهز شد: میانگین درشت با رقم اعشار فارسی («۴٫۳»)، ردیف ستاره‌ها و نمودار تفکیک ۵ به ۱ با نوارهای پیشرفت — به‌علاوه لینک ستاره‌ها + میانگین زیر عنوان محصول که مستقیم به بخش نظرات می‌پرد
  • کارت‌های محصول هم امتیاز نشان می‌دهند (ستاره، میانگین، تعداد نظر) در صفحهٔ اصلی، لیست محصولات و محصولات مشابه — یک اسکوپ withRating()، دو زیرکوئری؛ همان الگوی پرفروش‌ترین
  • قواعد حساب‌شده: هر کاربر برای هر محصول یک نظر (کلید یکتا + پیام دوستانه + مقاوم به ثبت همزمان)، فرم بعد از ثبت به جعبهٔ وضعیت تبدیل می‌شود، مهمان‌ها راهنمای ورود می‌بینند و auth + throttle:5,1 جلوی اسپم را می‌گیرد
  • انتخاب ستارهٔ راست‌به‌چپِ صیقلی با پیش‌نمایش hover و اعتبارسنجی در ReviewRequest اختصاصی (امتیاز ۱ تا ۵، متن ۵ تا ۲۰۰۰ کاراکتر، پیام‌های فارسی)
  • صفحهٔ جدید مدیریت نظرات در /admin/reviews: تب‌های وضعیت با شمارنده، دکمه‌های یک‌کلیکی تأیید / رد / حذف، لینک محصولات و بج «در انتظار تأیید» در سایدبار — قرینهٔ جریان سفارش‌ها

📊 اعداد

  • ۱۶۲ تست فیچر (۶۳۲ ادعا) — ۲۹ تست جدید نظرات: گاردهای دسترسی، اعتبارسنجی، جریان تأیید تا نمایش، رد نظر، قاعدهٔ تک‌نظر، محدودیت نرخ، رندر میانگین/تعداد/تفکیک، شمارش فقط تأییدشده‌ها، نشان خریدار تأییدشده (واقعی / غیرخریدار / سفارش لغوشده)، راهنمای ورود مهمان، جعبهٔ وضعیت، جریان‌های مدیریت، بج سایدبار و حذف آبشاری
  • سبک کد: Pint پاک روی ۱۱۰ فایل
  • یک مهاجرت جدید (reviews) — با php artisan shop:install یا migrate ساده اعمال می‌شود؛ بدون تغییر کانفیگ
راه‌اندازی سریع (بدون تغییر)
git clone https://github.com/MSS1625/shop-php.git && cd shop-php
composer install
php artisan shop:install     # env + کلید + مهاجرت + داده + لینک (شامل WELCOME10)
php artisan admin:create     # ساخت مدیر (تعاملی)
php artisan serve            # → http://localhost:8000
دیدن آن در عمل
# ثبت‌نام کنید، یک محصول را باز کنید و به «نظرات خریداران» بروید:
# ← روی ستاره‌ها hover کنید، ۵ ستاره را انتخاب کنید، نظر بنویسید و
#   «ثبت نظر» را بزنید — پیام «نظر شما ثبت شد و پس از تأیید…» + جعبهٔ وضعیت.
# ← به‌عنوان مدیر وارد شوید ← نظرات: سایدبار بج (۲) نشان می‌دهد؛
#   تب‌ها: در انتظار تأیید (۲) / تأییدشده (۶) — روی یک نظر در انتظار ✓ بزنید:
#   بلافاصله در صفحهٔ محصول می‌آید، با نشان «خریدار تأییدشده» برای خریداران واقعی.
# ← لیست محصولات حالا روی کارت‌های امتیازدار «★ ۴٫۳ (۴ نظر)» نشان می‌دهد.

🔍 v2.8.0 — Smart Product Discovery

Choose a tag to compare

@MSS1625 MSS1625 released this 24 Oct 06:52
4581e8a

🔍 v2.8.0 — Smart Product Discovery

دیجی‌شاپ — فروشگاه آنلاین تجهیزات شبکه | Digi-Shop — Online network-equipment store
Laravel 12 · PHP 8.2+ · Bootstrap 5 RTL · MIT License

🇬🇧 English

🔍 New — Price Range, Availability & Best-Selling Sort

  • The products page gained a price-range filter — minimum and maximum, each usable on its own — plus an in-stock-only toggle, and both combine freely with search, category and any sort order; active filters also survive every pagination link
  • Persian keyboard friendly, again: price inputs normalize Persian/Arabic digits and thousands separators, so ۵۰۰٬۰۰۰, 500,000 and 500000 all mean the same thing — the same approach as coupon codes and order tracking, now extracted into a tiny en_num() helper (the mirror of the existing fa_num())
  • Forgiving by design: if the range is entered backwards (min > max) it is silently swapped instead of returning an empty grid, and non-numeric input is simply ignored — no error pages, no lost context
  • New «پرفروش‌ترین» (best-selling) sort: products ranked by units sold through a single withSum subquery — items of cancelled orders never count (the same rule the admin dashboard follows), and ties break toward the newest product
  • Related products, upgraded: same category and active as before, but the random shuffle is gone — in-stock items now come first (newest first within each group) and out-of-stock ones only fill leftover slots; the section is deterministic and fully testable
  • A «فیلتر فعال» indicator with a one-click reset sits next to the result count, and the empty state offers a filter reset too — validation lives in a dedicated ProductFilterRequest

📊 Numbers

  • 133 feature tests (537 assertions) — 21 new discovery tests: price min/max/range, backwards-range swap, Persian digits & separators, graceful non-numeric input, in-stock filter, popular ranking, cancelled-item exclusion, newest tie-break, combined filters, pagination query-string, indicator/reset rendering, validation failures, inactive-product regression, and related-products ordering/exclusion/fallback
  • Code style: Pint clean across 109 files
  • Zero migrations, zero new config — drop-in upgrade
Quick start (unchanged)
git clone https://github.com/MSS1625/shop-php.git && cd shop-php
composer install
php artisan shop:install     # env + key + migrate + seed + link (includes WELCOME10)
php artisan admin:create     # interactive admin user
php artisan serve            # → http://localhost:8000
Seeing it in action
# Open http://localhost:8000/products
# → type ۵۰۰٬۰۰۰ in «حداقل قیمت» and ۵۰۰۰۰۰۰ in «حداکثر قیمت», tick
#   «فقط کالاهای موجود», hit اعمال — the grid narrows, the count shows
#   «فیلتر فعال», and pagination keeps everything.
# → Swap the two prices on purpose (big number first): same results.
# → Pick «پرفروش‌ترین» in the sort select: best sellers first.
# → Open any product: related items now show available ones first.

🇮🇷 فارسی

🔍 جدید — فیلتر بازهٔ قیمت، موجودی و مرتب‌سازی پرفروش‌ترین

  • صفحهٔ محصولات به فیلتر بازهٔ قیمت مجهز شد — حداقل و حداکثر، هر کدام به‌تنهایی هم کار می‌کند — به‌علاوهٔ گزینهٔ **«فقط کالاهای موجود»»؛ هر دو آزادانه با جستجو، دسته و هر مرتب‌سازی ترکیب می‌شوند و فیلترهای فعال در تمام لینک‌های صفحه‌بندی هم حفظ می‌شوند
  • باز هم سازگار با کیبورد فارسی: ورودی‌های قیمت ارقام فارسی/عربی و جداکنندهٔ هزارگان را نرمال می‌کنند — ۵۰۰٬۰۰۰ و 500,000 و 500000 هر سه یک معنی دارند؛ همان رویکرد کد تخفیف و پیگیری سفارش، این بار در قالب هلپر کوچک en_num() (قرینهٔ fa_num() موجود)
  • بخشنده در طراحی: اگر بازه برعکس وارد شود (حداقل > حداکثر) بی‌صدا جابه‌جا می‌شود تا نتیجهٔ خالی نبینید، و ورودی غیرعددی هم نادیده گرفته می‌شود — نه صفحهٔ خطا، نه بافتِ ازدست‌رفته
  • مرتب‌سازی جدید «پرفروش‌ترین»: رتبه‌بندی محصولات بر اساس تعداد فروش با یک زیرکوئری withSum — اقلام سفارش‌های لغوشده هرگز حساب نمی‌شوند (همان قاعدهٔ داشبورد مدیریت) و تساوی‌ها به نفع محصول جدیدتر می‌شکنند
  • محصولات مشابه، ارتقا یافت: هم‌دسته و فعال مثل قبل، اما چیدمان تصادفی حذف شد — موارد موجود اول می‌آیند (داخل هر گروه، جدیدترین اول) و ناموجودها فقط جاهای خالی را پر می‌کنند؛ بخش حالا قطعی و کاملاً قابل‌آزمون است
  • نشانگر «فیلتر فعال» با دکمهٔ حذف یک‌کلیکی کنار تعداد نتایج نشسته، حالت خالی هم دکمهٔ حذف فیلترها دارد — و اعتبارسنجی در ProductFilterRequest اختصاصی منتقل شده

📊 اعداد

  • ۱۳۳ تست فیچر (۵۳۷ ادعا) — ۲۱ تست جدید کشف محصول: حداقل/حداکثر/بازهٔ قیمت، جابه‌جایی بازهٔ معکوس، ارقام فارسی و جداکننده، ورودی غیرعددی، فیلتر موجودی، رتبه‌بندی پرفروش، حذف اقلام لغوشده، شکست تساوی با جدیدترین، ترکیب فیلترها، رشتهٔ کوئری صفحه‌بندی، نمایش نشانگر و دکمهٔ حذف، خطاهای اعتبارسنجی، رگرسیون محصولات غیرفعال و ترتیب/حذف/جانشینی محصولات مشابه
  • سبک کد: Pint پاک روی ۱۰۹ فایل
  • بدون مهاجرت دیتابیس، بدون کانفیگ جدید — ارتقای درجا
راه‌اندازی سریع (بدون تغییر)
git clone https://github.com/MSS1625/shop-php.git && cd shop-php
composer install
php artisan shop:install     # env + کلید + مهاجرت + داده + لینک (شامل WELCOME10)
php artisan admin:create     # ساخت مدیر (تعاملی)
php artisan serve            # → http://localhost:8000
دیدن آن در عمل
# http://localhost:8000/products را باز کنید
# ← در «حداقل قیمت» بنویسید ۵۰۰٬۰۰۰ و در «حداکثر قیمت» ۵۰۰۰۰۰۰،
#   «فقط کالاهای موجود» را تیک بزنید و «اعمال» را بزنید —
#   شبکه محدود می‌شود، کنار تعداد «فیلتر فعال» می‌آید و
#   صفحه‌بندی همه‌چیز را حفظ می‌کند.
# ← عمداً دو قیمت را برعکس وارد کنید (عدد بزرگ اول): همان نتایج.
# ← در مرتب‌سازی «پرفروش‌ترین» را انتخاب کنید: پرفروش‌ها اول.
# ← یک محصول را باز کنید: مشابه‌های موجود اول نمایش داده می‌شوند.

🚚 v2.7.0 — Guest Order Tracking

Choose a tag to compare

@MSS1625 MSS1625 released this 21 Oct 14:17
9db6adc

🚚 v2.7.0 — Guest Order Tracking

دیجی‌شاپ — فروشگاه آنلاین تجهیزات شبکه | Digi-Shop — Online network-equipment store
Laravel 12 · PHP 8.2+ · Bootstrap 5 RTL · MIT License

🇬🇧 English

🚚 New — Order Tracking Without an Account

  • New public order-tracking page at /track — customers who checked out as guests (no account) can now follow their order with just two things they already have: the order number and the mobile number they entered at checkout — both must match, one alone is never enough
  • The result shows the same 4-step timeline as the account area (pending → processing → shipped → delivered), the payment status card, the full items list with prices and the discount line — cancelled orders show their cancelled state
  • Payment retry for guests: after a successful lookup, an unpaid online order shows the pay / retry button — powered by the exact viewed_orders session mechanism checkout already uses, so no new authorization surface was added
  • Security-first design:
    • Uniform error message for a wrong order number and a wrong phone — the response never reveals which field was valid (anti-enumeration)
    • Rate limited like the login form (10 lookups per minute) — brute-forcing order numbers is impractical
    • Minimum-data page: the recipient's address is never shown and the mobile number is masked (۰۹۱۲***۷۸۹)
    • The lookup result lives in the session for 30 minutes only, and the flow follows Post/Redirect/Get — refreshing the page never re-submits the form
  • Persian keyboard friendly: Persian/Arabic digits are normalized on both fields (same approach as coupon codes) — DS-۲۶۱۰۲۱-... typed on a Persian keyboard finds the order
  • The navbar gained a «پیگیری سفارش» link, and the order-success page now guides guests to the tracking page for future visits

📊 Numbers

  • 112 feature tests (476 assertions) — 16 new tracking tests: correct/wrong/mismatched lookups, identical-error anti-enumeration check, Persian-digit normalization, session expiry, rate limiting, cancelled state, payment-retry access grant, address-hiding, phone masking, success-page guidance
  • Code style: Pint clean across 108 files
Quick start (unchanged)
git clone https://github.com/MSS1625/shop-php.git && cd shop-php
composer install
php artisan shop:install     # env + key + migrate + seed + link (includes WELCOME10)
php artisan admin:create     # interactive admin user
php artisan serve            # → http://localhost:8000
Seeing it in action
# Place an order as a guest (checkout without logging in) and note the
# order number (DS-YYMMDD-XXXX) and the mobile number you typed.
# Then open http://localhost:8000/track and enter both —
# the timeline, payment status and items appear.
# Try a wrong phone: the same friendly error, zero order data.

🇮🇷 فارسی

🚚 جدید — پیگیری سفارش بدون حساب کاربری

  • صفحهٔ عمومی پیگیری سفارش در آدرس /track — مشتریانی که به‌صورت مهمان (بدون حساب) خرید کرده‌اند حالا با همان دو چیزی که از قبل دارند سفارششان را دنبال می‌کنند: شماره سفارش و شماره موبایلی که هنگام خرید وارد کرده‌اند — هر دو باید همزمان بخوانند، یکی به‌تنهایی هرگز کافی نیست
  • نتیجه همان تایم‌لاین ۴ مرحله‌ای بخش حساب کاربری را نشان می‌دهد (در انتظار بررسی ← در حال پردازش ← ارسال شده ← تحویل داده شده)، به‌همراه کارت وضعیت پرداخت، فهرست کامل اقلام با قیمت و سطر تخفیف — سفارش‌های لغوشده هم وضعیت لغو خود را نشان می‌دهند
  • تلاش مجدد پرداخت برای مهمان‌ها: بعد از پیگیری موفق، سفارش آنلاینِ پرداخت‌نشده دکمهٔ پرداخت/تلاش مجدد را نشان می‌دهد — با دقیقاً همان مکانیزم سشن viewed_orders که تسویه‌حساب استفاده می‌کند؛ یعنی هیچ سطح دسترسی جدیدی به پروژه اضافه نشد
  • طراحی امنیت‌محور:
    • پیام خطای یکسان برای شماره سفارش غلط و موبایل غلط — پاسخ هرگز لو نمی‌دهد کدام فیلد درست بوده (ضد enumeration)
    • محدودیت نرخ مثل فرم ورود (۱۰ جستجو در دقیقه) — حدس زدن شماره سفارش‌ها عملاً بی‌فایده است
    • صفحهٔ حداقلِ داده: آدرس گیرنده هرگز نمایش داده نمی‌شود و شماره موبایل ماسک می‌شود (۰۹۱۲***۷۸۹)
    • نتیجهٔ پیگیری فقط ۳۰ دقیقه در سشن می‌ماند و جریان Post/Redirect/Get است — رفرش صفحه هرگز فرم را دوباره ارسال نمی‌کند
  • سازگار با کیبورد فارسی: ارقام فارسی/عربی هر دو فیلد نرمال می‌شوند (همان رویکرد کد تخفیف) — با کیبورد فارسی هم DS-۲۶۱۰۲۱-... پیدا می‌شود
  • نوبار به لینک «پیگیری سفارش» مجهز شد و صفحهٔ موفقیت سفارش حالا مهمان‌ها را برای پیگیری‌های بعدی راهنمایی می‌کند

📊 اعداد

  • ۱۱۲ تست فیچر (۴۷۶ ادعا) — ۱۶ تست جدید پیگیری: جستجوی درست/غلط/ناهماهنگ، بررسی پیام یکسان ضد enumeration، نرمال‌سازی ارقام فارسی، انقضای سشن، محدودیت نرخ، وضعیت لغو، دسترسی تلاش مجدد پرداخت، مخفی‌سازی آدرس، ماسک موبایل و راهنمای صفحه موفقیت
  • سبک کد: Pint پاک روی ۱۰۸ فایل
راه‌اندازی سریع (بدون تغییر)
git clone https://github.com/MSS1625/shop-php.git && cd shop-php
composer install
php artisan shop:install     # env + کلید + مهاجرت + داده + لینک (شامل WELCOME10)
php artisan admin:create     # ساخت مدیر (تعاملی)
php artisan serve            # → http://localhost:8000
دیدن آن در عمل
# بدون ورود، یک سفارش ثبت کنید و شماره سفارش (DS-YYMMDD-XXXX)
# و موبایلی که وارد کردید را یادداشت کنید.
# سپس http://localhost:8000/track را باز کنید و هر دو را وارد کنید —
# تایم‌لاین، وضعیت پرداخت و اقلام ظاهر می‌شوند.
# یک بار هم موبایل اشتباه بزنید: همان پیام دوستانه، بدون هیچ داده‌ای از سفارش.