Repository navigation
Releases
v2.16.0 — داکر: کل فروشگاه در یک دستور (Docker: the whole shop in one command)
🐳 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, thenphp:8.3-fpm-alpine+ nginx) runs twice — theappservice is php-fpm, thewebservice is the same image in web role serving static files and proxying PHP toapp:9000by service name;dbis the officialmysql:8.4with 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 + adocker/await-db.phpretry loop —depends_onalone only guarantees order), then runs the same idempotentphp artisan shop:installthe project has had since v1.0.0 — restart-safe, no duplicate data - Data survives rebuilds in the
db-dataandstorage-datavolumes; host port changeable viaDIGISHOP_PORT; admin password printed once indocker compose logs app(or setADMIN_PASSWORDin.env.docker) - Tests inside a container, zero host tooling:
docker compose run --rm testruns all 304 tests on SQLite in-memory - New
dockerjob in CI proves the image on every push and PR: build → healthcheck-gated compose up → homepage smoke test (title + seeded product) → full suite in thetestcontainer .dockerignorekeeps the image lean and excludes the machine-localpublic/storagesymlink (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.ymlgained thedockerjob (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 wipeOn 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 thetestcontainer
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:installidempotent را اجرا میکند که از 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)
🤖 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 newstatic-analysisjob 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
@mixinof its model — so$order->items,$item->productand$this->idinside 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,
AdminUserSeederreads the admin password viaconfig()(plainenv()breaks underconfig:cache), and the stockExampleTestplaceholder — an always-true assertion — is gone - Live coverage badge, zero third-party services: on every push to
maina 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 thecoverage-badgebranch — 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 onmain - Auto-release: push a tag
v*andrelease.ymlruns the whole test suite (a broken tag never ships), extracts the bilingual notes for that version fromCHANGELOG.mdviascripts/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) andcomposer 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:installhas 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 runsOn 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)andCoverage + Badge - when
Coverage + Badgefinishes, the Coverage badge in the README stops saying "inaccessible" and shows the real percentage — and updates itself on every push tomain - 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)
🔑 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/tokenslists 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:installhas 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 inTokenManagementTest
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:8000Seeing 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)
⚖️ 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 awithCountsubquery — 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
fullflag in the JSON; inactive products 404 on toggle;POST /compare/clearempties the whole list; the/compare/clearroute 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:installhas 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 inCompareTest
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:8000Seeing 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)
📡 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/loginnames the token after yourdevice_name;POST /auth/logoutrevokes 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 /productsruns 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 /orderstakesitems: [{slug, quantity}], customer details, payment method and an optionalcoupon_code— everything runs through the new sharedPlaceOrderservice (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 apayment_urlfor 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
GETendpoints for reviews and questions (approved/answered only, verified badges included);POSTendpoints 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 carryis_in_wishlistfor logged-in tokens (one query per page) - Guardrails: 60 req/min per token (
throttle:api), 10/min on login/register, standard 422 validation shape,409for 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 byshop:install);symfony/mimebumped 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:installor plainmigratehandles 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:8000Seeing 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"</...
❓ v2.11.0 — Product Questions & Answers
❓ 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/questionspanel 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,1plus 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:installor plainmigratehandles 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:8000Seeing 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
❤️ 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
/wishlistpage: 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
Wishlistsingleton 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:installor plainmigratehandles 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:8000Seeing 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
⭐ 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,1keep 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:installor plainmigratehandles 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:8000Seeing 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
🔍 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,000and500000all mean the same thing — the same approach as coupon codes and order tracking, now extracted into a tinyen_num()helper (the mirror of the existingfa_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
withSumsubquery — 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:8000Seeing 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
🚚 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_orderssession 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:8000Seeing 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 را باز کنید و هر دو را وارد کنید —
# تایملاین، وضعیت پرداخت و اقلام ظاهر میشوند.
# یک بار هم موبایل اشتباه بزنید: همان پیام دوستانه، بدون هیچ دادهای از سفارش.