Skip to content
Merged

Shop #55

Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
49 commits
Select commit Hold shift + click to select a range
2cda481
Merge pull request #51 from bahman026/main
bahman026 Jun 24, 2026
8a45780
feat(shop): foundation - SEO, footer data, icons, error pages
bahman026 Jun 24, 2026
91f5129
fix(shop): correct production DB target, canonical URLs, and test DB
bahman026 Jun 24, 2026
dc8fd08
Merge pull request #52 from bahman026/implement_fundation
bahman026 Jun 24, 2026
838fbe1
feat(shop): home page + variety images, fix placeholder images and seeds
bahman026 Jun 24, 2026
691aa39
feat(shop): product detail page + fix variety stock seeding
bahman026 Jun 24, 2026
12c5060
feat(shop): multi-axis variety selection and gallery fixes
bahman026 Jun 25, 2026
988fae4
docs(shop): document variety quantity cap rule
bahman026 Jun 25, 2026
f0a2582
feat(shop): combined gallery, stock-capped quantity, variety edge cases
bahman026 Jun 27, 2026
1a388b1
refactor(shop): split product page into actions + DTOs
bahman026 Jun 27, 2026
145d782
refactor(shop): home page actions + shared product card
bahman026 Jun 27, 2026
bccbfe3
chore(shop): add ESLint/Prettier and enforce 100% type coverage
bahman026 Jun 27, 2026
da1ea6c
Merge pull request #53 from bahman026/implement_home_page
bahman026 Jun 27, 2026
3b1a1e9
feat(shop): category listing page with faceted filters
bahman026 Jun 27, 2026
a17b680
feat(shop): product card variety-image fallback and open in new tab
bahman026 Jun 27, 2026
140bd61
docs(shop): document filters, card behavior, SSR runtime and tags
bahman026 Jun 27, 2026
bbdab96
docs: rewrite root README with project overview
bahman026 Jun 27, 2026
1faecf1
feat(shop): brand page with category/price/availability filters
bahman026 Jun 27, 2026
fbe75a9
feat(shop): CMS pages + FAQ at clean URLs, working footer links
bahman026 Jun 27, 2026
64850b0
feat(shop): sitemap.xml, robots.txt and per-page SEO
bahman026 Jun 27, 2026
72529fd
docs(shop): require separate atomic commits in AGENTS.md
bahman026 Jun 27, 2026
4f01f4d
feat(shop): product search with header autocomplete
bahman026 Jun 27, 2026
a916213
feat(shop): mobile login/register with OTP or password
bahman026 Jun 27, 2026
8680983
feat(shop): account area with dashboard and profile
bahman026 Jun 27, 2026
3c697a0
feat(shop): address management with Neshan map
bahman026 Jun 27, 2026
ca4a8b5
feat(shop): PWA manifest and iOS add-to-home-screen prompt
bahman026 Jun 27, 2026
20d2fb5
feat(shop): cart and checkout shipping flow
bahman026 Jun 27, 2026
562727b
docs(admin): add missing settings.autoload cache entry
bahman026 Jul 23, 2026
0f590c3
fix(admin): add orders.address_id — schema had no way to record shipp…
bahman026 Jul 23, 2026
7144e42
docs(shop): resync stale IMPLEMENTATION.md mirror with admin's
bahman026 Jul 23, 2026
a60ebb4
feat(shop): order creation + Zarinpal sandbox payment checkout
bahman026 Jul 23, 2026
bfa2269
fix(shop): category filter facets respect attribute_groups.order
bahman026 Jul 23, 2026
0794f3a
fix(shop): product specs/highlights keep their attribute-group label
bahman026 Jul 23, 2026
86004b2
fix(shop): deterministic variant axis/option ordering in the variety …
bahman026 Jul 23, 2026
dc244e2
fix(admin): sort Ancestor/AttributeGroup tables by their configured o…
bahman026 Jul 23, 2026
04d46b1
fix(admin): remove non-existent order column from attribute_group_cat…
bahman026 Jul 23, 2026
eb1f416
docs(admin): clarify Receipts vs Transactions split in panel subheadings
bahman026 Jul 23, 2026
d40aead
chore: docker-compose cleanup
bahman026 Jul 23, 2026
70aedc6
chore: add root .mcp.json for laravel-boost MCP server
bahman026 Jul 23, 2026
326e71d
feat(admin): add order tracking code
bahman026 Jul 23, 2026
8d738a6
fix(admin): fix seeder FK-order bugs, stop TestSeeder clobbering real…
bahman026 Jul 23, 2026
2907556
feat(shop): account order history, tracking codes, pay-again retry, a…
bahman026 Jul 23, 2026
1946b5c
fix(shop): allow a user with no mobile to load the account area
bahman026 Jul 23, 2026
acfd2e3
feat(shop): downloadable receipt and order-detail redesign
bahman026 Jul 23, 2026
df5d402
feat(shop): wishlist (add/remove on product page, account list)
bahman026 Jul 23, 2026
ad5ea1e
Merge pull request #54 from bahman026/implement_category_listing
bahman026 Jul 24, 2026
1060bf1
feat(admin): add 1-5 rating column to reviews
bahman026 Jul 24, 2026
bb5fc51
feat(shop): product review submission with ratings and verified-buyer…
bahman026 Jul 24, 2026
41d723c
test(shop): pin ZARINPAL_* config in phpunit.xml so checkout tests pa…
bahman026 Jul 24, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
43 changes: 43 additions & 0 deletions .github/workflows/deploy-application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,33 @@ jobs:
create-deployment-artifact-shop:
name: Create Deployment Artifact (Shop)
runs-on: ubuntu-latest

# The shop reads the admin-owned database, so tests need that schema.
# We stand up Postgres, build the schema with admin's migrations, then run
# the shop test suite against it.
services:
postgres:
image: postgres:16
env:
POSTGRES_DB: shop_flow_test
POSTGRES_USER: shop_flow
POSTGRES_PASSWORD: password
ports:
- 5432:5432
options: >-
--health-cmd "pg_isready -U shop_flow"
--health-interval 10s
--health-timeout 5s
--health-retries 5

env:
DB_CONNECTION: pgsql
DB_HOST: 127.0.0.1
DB_PORT: 5432
DB_DATABASE: shop_flow_test
DB_USERNAME: shop_flow
DB_PASSWORD: password

steps:
- uses: actions/checkout@v2

Expand All @@ -61,6 +88,16 @@ jobs:
with:
php-version: 8.5
tools: composer
extensions: pdo_pgsql, pgsql

- name: Build shared schema (admin migrations)
working-directory: ./admin
run: |
composer install --no-interaction --prefer-dist --optimize-autoloader
cp .env.example .env
php artisan key:generate
php artisan migrate --force
php artisan db:seed --class="Database\\Seeders\\SettingSeeder" --force

- name: Install PHP Dependencies
working-directory: ./shop
Expand All @@ -73,6 +110,12 @@ jobs:
npm ci
npm run build

- name: Run Frontend Lint & Format Check
working-directory: ./shop
run: |
npm run lint
npm run format:check

- name: Setup Environment
working-directory: ./shop
run: |
Expand Down
19 changes: 19 additions & 0 deletions .mcp.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
"mcpServers": {
"laravel-boost": {
"type": "stdio",
"command": "docker",
"args": [
"exec",
"-i",
"-u",
"www-data",
"shop_flow_admin_app",
"php",
"artisan",
"boost:mcp"
],
"env": {}
}
}
}
122 changes: 110 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,19 +1,117 @@
### Documant of the database:
https://docs.google.com/document/d/e/2PACX-1vTqah2hdQeeiu3Le07zfOfp5vK-ojLwJtQmzbgdoq_wmJu-0dBdTcFsS0uSiUtYpSglEwMD5xSFIiG5/pub
### Setting Up and Running the Project in Docker:
# ShopFlow

ShopFlow is an **open-source, single-vendor e-commerce platform** (MIT licensed) for building a Persian, right-to-left online store. It is designed for one business selling its own products (no marketplace/seller system), with a strong focus on SEO and a server-rendered storefront so pages are fast and indexable.

It is built as a monorepo of two Laravel apps that share one PostgreSQL database:

**In the `infrastructure/docker` directory, create a `.env` file and fill in the values according to `.env.example`. Then run the following command to create the Postgres and Redis containers:**
```bash
sudo docker compose up -d --build
- **`admin/`** — the management panel (Laravel 13 + Filament 5, PHP 8.5). It **owns the database schema** (all migrations live here) and is where the team manages catalog, orders, content, and settings.
- **`shop/`** — the customer-facing storefront (Laravel 13 + Inertia + Vue 3 with SSR, PHP 8.5). It mostly **reads** catalog/pricing data and writes carts, orders, addresses, and payments. The UI is Persian, RTL-first.

The two apps never duplicate tables: `admin` migrates the shared schema, and `shop` adds read-focused Eloquent models that map to the same tables.

**note:** Make sure to configure the database settings in the .env files for both Admin and API projects as follows, if you haven't changed the host and port in the Docker Compose files:
```bash
DB_CONNECTION=pgsql
DB_HOST=db
DB_PORT=5432
## What ShopFlow provides

The platform models a full online-store domain:

- **Catalog** — hierarchical categories, products with purchasable varieties (e.g. size/color), brands, attributes and attribute groups, and images.
- **Discovery** — faceted category filtering (brand, attribute, price, availability), sorting, pagination, and attribute-based SEO landing pages (tags).
- **Pricing & promotions** — per-variety pricing, sale prices, discounts and coupons.
- **Cart & checkout** — carts, orders with line snapshots, per-city shipping methods, and inventory that is only decremented on a successful payment.
- **Payments** — manual receipts (card-to-card / Paya) and online gateways (Mellat / Parsian / Zarinpal) via transactions.
- **Customers** — accounts, addresses kept as immutable history, wishlists, product reviews, points, and newsletters.
- **SEO** — SSR HTML, unique titles/meta, canonical URLs, Open Graph, JSON-LD structured data, sitemap, and redirects.

The admin panel covers the full schema today. The storefront is built feature by feature against `shop/docs/STOREFRONT_IMPLEMENTATION.md`; see that roadmap for current status.

## Repository structure

```
ShopFlow/
├── admin/ # Filament admin panel (owns the DB schema)
├── shop/ # Inertia + Vue storefront (SSR)
├── infrastructure/
│ └── docker/ # Shared Postgres + Redis (docker compose)
├── .github/workflows/ # CI (deploy-application.yml)
└── README.md
```

## Tech stack

- PHP 8.5, Laravel 13
- Admin: Filament 5
- Storefront: Inertia.js 3 + Vue 3 (SSR), Tailwind CSS v4, FontAwesome
- PostgreSQL (shared), Redis
- Quality: Pest, Pint, PHPStan (level 5), 100% type coverage, ESLint + Prettier

## Database

`admin` is the single source of truth for the schema. The full table reference lives in:

- In-repo: `admin/docs/ShoFlow db doc.md` (and a copy under `shop/docs/`)
- Online: https://docs.google.com/document/d/e/2PACX-1vTqah2hdQeeiu3Le07zfOfp5vK-ojLwJtQmzbgdoq_wmJu-0dBdTcFsS0uSiUtYpSglEwMD5xSFIiG5/pub

Run migrations and seeders from `admin/` only. The storefront must not migrate these tables.

## Getting started

### 1. Shared services (Postgres + Redis)

In `infrastructure/docker`, create a `.env` from `.env.example`, then start the containers:

```bash
cd infrastructure/docker
sudo docker compose up -d --build
```

### 2. Configure each app

In both `admin/.env` and `shop/.env`, point the database at the shared Postgres (matching the values from `infrastructure/docker/.env`):

```bash
DB_CONNECTION=pgsql
DB_HOST=db
DB_PORT=5432
# DB_DATABASE / DB_USERNAME / DB_PASSWORD must match infrastructure/docker/.env
```

### 3. Admin (schema owner — set up first)

```bash
cd admin
composer install
php artisan key:generate
php artisan migrate --seed
npm install && npm run build
```

### 4. Storefront

```bash
cd shop
composer install
php artisan key:generate
npm install && npm run build
```

Fill in the values for DB_DATABASE, DB_USERNAME, and DB_PASSWORD according to the .env file in the infrastructure directory.
For app-specific details (Docker containers, SSR, conventions), see each app's own `README.md`, `AGENTS.md`, and `docs/`.

## Testing & quality

The storefront bundles all checks into one command (run inside its container):

```bash
cd shop
composer test-dev # Pest, Pint, Pest type-coverage (--min=100), PHPStan, ESLint, Prettier
```

CI runs the same checks via `.github/workflows/deploy-application.yml`.

## Documentation

- `shop/docs/STOREFRONT_IMPLEMENTATION.md` — storefront roadmap and status
- `admin/docs/` and `shop/docs/` — schema, variety guide, orders/inventory, cache keys, tags
- `shop/AGENTS.md` / `admin/AGENTS.md` — conventions for contributors and AI agents

## License

Open-sourced under the [MIT license](LICENSE).
8 changes: 6 additions & 2 deletions admin/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ This project has domain-specific skills available in `**/skills/**`. You MUST ac
# Test Enforcement

- Every change must be programmatically tested. Write a new test or update an existing test, then run the affected tests to make sure they pass.
- Run the minimum number of tests needed to ensure code quality and speed. Use `php artisan test --compact` with a specific filename or filter.
- Run the minimum number of tests needed to ensure code quality and speed. Use Pest directly (not `php artisan test`): `vendor/bin/pest` with a specific filename or `--filter`.

=== laravel/core rules ===

Expand Down Expand Up @@ -158,7 +158,7 @@ This project has domain-specific skills available in `**/skills/**`. You MUST ac

- This project uses Pest for testing. Create tests: `php artisan make:test --pest {name}`.
- The `{name}` argument should not include the test suite directory. Use `php artisan make:test --pest SomeFeatureTest` instead of `php artisan make:test --pest Feature/SomeFeatureTest`.
- Run tests: `php artisan test --compact` or filter: `php artisan test --compact --filter=testName`.
- Run tests with Pest directly (not `php artisan test`): `vendor/bin/pest` or filter: `vendor/bin/pest --filter=testName`.
- Do NOT delete tests without approval.

=== filament/filament rules ===
Expand Down Expand Up @@ -444,6 +444,8 @@ When adding a new entity, build the files in this order, matching the existing f
- `modifyQueryUsing` for relationship selects is the **3rd parameter** of `->relationship()`, not a chainable method: `->relationship('name', 'title', fn (Builder $q): Builder => $q->with('relation'))`. Calling `->modifyQueryUsing()` as a separate method throws `BadMethodCallException`.
- `->getOptionLabelFromRecordUsing(fn (Model $record): string => ...)` customises the label shown for each option in a relationship select. Pair with eager-loading in the `modifyQueryUsing` closure to avoid N+1.
- Control navigation order within a group with `protected static ?int $navigationSort = 1;` (lower = higher in the list).
- **A model's own `order` column must be paired with `->defaultSort('order')` on its table.** A sortable `order` column alone (e.g. `AncestorResource`, `AttributeGroupResource`) does nothing by default — the list still renders in insertion/id order every time it's opened, silently defeating the whole point of the field. See `FaqResource` for the reference pattern.
- **Never `withPivot()` a column that isn't actually migrated on the pivot table.** `AttributeGroup::categories()`/`Category::attributeGroups()` both declared a `order` pivot column that was never added to `attribute_group_category`, which threw `SQLSTATE[42703]: undefined column` the instant the relation was queried — verify pivot columns against the actual migration, not just intent.
- Add an explanatory subheading to a list page by overriding `mount()` on the `ListRecords` class: set `protected ?string $subheading = null;` and assign `$this->subheading = trans('resource.subheading');` inside `mount()`. Never use a hard-coded string — dynamic assignment is required for locale switching.
- Add a tooltip to a form field with `->hintIcon('heroicon-o-information-circle')->hintIconTooltip('Explanation...')`. Use this instead of always-visible `->hint()` when the text is long.
- Always add `->image()` to `FileUpload` fields that accept images. This restricts the file picker to image types only.
Expand Down Expand Up @@ -511,6 +513,8 @@ When adding a new entity, build the files in this order, matching the existing f
- `TestSeeder` holds factory-generated sample data (`Model::factory()->count(20)->create()`) for manual admin-panel testing. Run it separately with `php artisan db:seed --class=TestSeeder`. Add new sample-data seeders here, not in `DatabaseSeeder`.
- Reference seeders use idempotent `updateOrCreate()` / `firstOrCreate()` so re-seeding is safe.
- When truncating and re-seeding a table whose model has a `deleting` event (e.g. to cascade-delete related images), delete records one by one via `Model::all()->each->delete()` BEFORE truncating the parent. Use `->each->delete()` on a **Collection**, not a query builder — `Model::query()->each` does not exist and will throw an exception.
- **A "delete-then-recreate" seeder must clear every table that FK-references it first, not just its own model's `deleting` event.** `ShippingLineSeeder` deleting all `shipping_lines` threw `SQLSTATE[23503]` because `shipping_methods`/`shipping_cities` (no cascade) still referenced them; `CitySeeder` deleting `cities` hit the same thing via `addresses.city_id` (worse: `Address` uses `SoftDeletes`, so even a soft-deleted row still blocks the FK — use `withTrashed()->forceDelete()`, not a plain `delete()`, to actually clear it). Check every migration for FKs into the table you're about to wipe, not just the ones you already know about.
- **`TestSeeder` must never seed a table that a "real" seeder (`DatabaseSeeder`'s chain) already populates with load-bearing data.** `TestSeeder` used to include `ShippingLineSeeder`/`ShippingMethodSeeder`/`ShippingCitySeeder` (20 random rows each); since `DatabaseSeeder` → `ShippingSeeder` already seeds the 3 real, checkout-critical shipping methods, running `TestSeeder` afterward silently replaced them with random fake ones tied to random specific cities (no nationwide fallback) — breaking the storefront checkout's shipping-method selection with no visible error until a customer tried to check out. Removed from `TestSeeder` entirely; re-run `ShippingSeeder` if this ever regresses.
- Read configurable values from config, not literals (see `AdminSeeder` reading `config('admin.account')`).

## Pest tests
Expand Down
1 change: 1 addition & 0 deletions admin/app/Filament/Resources/AncestorResource.php
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ public static function form(Schema $schema): Schema
public static function table(Table $table): Table
{
return $table
->defaultSort('order')
->columns([
TextColumn::make('name')
->label(trans('ancestor.name'))
Expand Down
1 change: 1 addition & 0 deletions admin/app/Filament/Resources/AttributeGroupResource.php
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ public static function form(Schema $schema): Schema
public static function table(Table $table): Table
{
return $table
->defaultSort('order')
->columns([
TextColumn::make('ancestor.name')
->label(trans('attribute_group.ancestor'))
Expand Down
10 changes: 10 additions & 0 deletions admin/app/Filament/Resources/OrderResource.php
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,12 @@ public static function form(Schema $schema): Schema
]),
Fieldset::make(trans('order.section_shipping'))
->schema([
Select::make('address_id')
->label(trans('order.address_id'))
->relationship('address', 'address')
->searchable()
->preload()
->native(false),
Select::make('shipping_line_id')
->label(trans('order.shipping_line_id'))
->relationship('shippingLine', 'name')
Expand Down Expand Up @@ -208,6 +214,10 @@ public static function table(Table $table): Table
TextColumn::make('id')
->label('#')
->sortable(),
TextColumn::make('tracking_code')
->label(trans('order.tracking_code'))
->searchable()
->copyable(),
TextColumn::make('user.email')
->label(trans('order.user_id'))
->searchable()
Expand Down
23 changes: 23 additions & 0 deletions admin/app/Filament/Resources/ProductResource.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
use App\Models\AttributeGroup;
use App\Models\AttributeGroupCategory;
use App\Models\Product;
use App\Models\Variety;
use Filament\Actions\BulkActionGroup;
use Filament\Actions\DeleteBulkAction;
use Filament\Actions\EditAction;
Expand All @@ -27,6 +28,7 @@
use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\Toggle;
use Filament\Resources\Resource;
use Filament\Schemas\Components\Fieldset;
use Filament\Schemas\Components\Utilities\Get;
use Filament\Schemas\Components\Utilities\Set;
use Filament\Schemas\Schema;
Expand Down Expand Up @@ -356,6 +358,27 @@ public static function form(Schema $schema): Schema
->required()
->options(VarietyStatusEnum::options())
->default(VarietyStatusEnum::PUBLISHED->value),
Fieldset::make(trans('product.variety_image'))
->relationship('image')
->schema([
FileUpload::make('path')
->label(trans('product.path'))
->image()
->nullable()
->columnSpanFull(),
TextInput::make('alt_text')
->label(trans('product.alt_text'))
->nullable()
->maxLength(255),
])
->mutateRelationshipDataBeforeSaveUsing(function (array $data, Variety $record): array {
if (empty($data['path'])) {
$record->image?->delete();
}

return $data;
})
->columnSpanFull(),
])
->columnSpanFull(),
]);
Expand Down
11 changes: 11 additions & 0 deletions admin/app/Filament/Resources/ReviewResource.php
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,13 @@ public static function form(Schema $schema): Schema
->columnSpanFull()
->hintIcon('heroicon-o-information-circle')
->hintIconTooltip(trans('review.content_hint')),
Select::make('rating')
->label(trans('review.rating'))
->options([1 => '۱', 2 => '۲', 3 => '۳', 4 => '۴', 5 => '۵'])
->nullable()
->native(false)
->hintIcon('heroicon-o-information-circle')
->hintIconTooltip(trans('review.rating_hint')),
Select::make('product_id')
->label(trans('review.product_id'))
->relationship('product', 'heading')
Expand Down Expand Up @@ -139,6 +146,10 @@ public static function table(Table $table): Table
->label(trans('review.heading'))
->limit(40)
->searchable(),
TextColumn::make('rating')
->label(trans('review.rating'))
->placeholder('—')
->sortable(),
TextColumn::make('user.email')
->label(trans('review.user'))
->placeholder(trans('review.anonymous'))
Expand Down
Loading
Loading