Conversation
pr to shop
- Wire shop to the shared admin-owned Postgres via a read-only Setting model; render the global footer (links, contact, socials, about, copyright) from autoloaded settings through Inertia shared props. - Add SEO scaffolding: AppHead component, shared meta defaults, canonical, Open Graph/Twitter, robots.txt. - Add useFormat composable (Persian digits, Jalali date, money). - Add Persian RTL 404/500 error pages. - Add self-hosted FontAwesome behind a shared Icon component and make it the required icon pattern in AGENTS.md. Co-authored-by: Cursor <cursoragent@cursor.com>
- .env.example now defaults to the shared Postgres (was sqlite) so deploys and clones don't silently fall back to an empty database - build canonical/og:url from APP_URL and trust proxy headers so SEO URLs stay correct behind a TLS-terminating proxy - run shop tests against the admin-owned schema in Postgres via DatabaseTransactions, with CI building the schema from admin migrations Co-authored-by: Cursor <cursoragent@cursor.com>
Implement fundation
- shop: storefront home page (hero slider, categories, banners, product carousels, brands) with header nav, SEO/JSON-LD, and canonical URLs - admin/shop: add polymorphic image to varieties (model, factory, seeder, Filament form/table, product repeater, shop read model) - admin: replace dead via.placeholder.com URLs with placehold.co in factories - admin: fix TestSeeder unique constraint via Postgres sequence sync and reuse of existing province/city records; seed product/variety images Co-authored-by: Cursor <cursoragent@cursor.com>
- shop: ProductController@show (/products/{slug}) + Product/Show.vue with
gallery, variety selector, buy box, specs, reviews, breadcrumbs, related
carousel; Product/Offer + BreadcrumbList JSON-LD; view counter; tests
- admin: ProductSeeder/VarietySeeder now attach published, in-stock
varieties to products (adds published()/inStock() factory states) so the
storefront no longer shows everything as out of stock
- docs: run tests via Pest directly instead of php artisan test
Co-authored-by: Cursor <cursoragent@cursor.com>
- Primary attribute group (e.g. color) drives variety selection; additional attributes (e.g. size) are constrained by the chosen primary, never block it - Hide price until a variety is fully selected; show a prompt instead - Gallery shows the selected variety's image first and dedupes by URL Co-authored-by: Cursor <cursoragent@cursor.com>
Quantity can never exceed the selected variety's inventory; noted in AGENTS.md business constraints and the storefront roadmap. Co-authored-by: Cursor <cursoragent@cursor.com>
- Gallery shows all product + variety images combined; selecting a variety switches the main image instead of filtering the list - Cap quantity at the selected variety's inventory; reset on variety change - Show out-of-stock state when no variety is buyable - Pick the primary attribute axis deterministically (most-voted group) - Auto-select a lone flat variety so price shows without a click Co-authored-by: Cursor <cursoragent@cursor.com>
Thin ProductController that delegates to single-purpose actions under app/Actions; per-model DTOs (Product, Variety, Image, Review) under app/DTOs, with value shapes kept as typed arrays. Documents the controller/action/DTO conventions in AGENTS.md. Co-authored-by: Cursor <cursoragent@cursor.com>
Slim HomeController to delegate to per-section actions under Actions/Home; add Actions/Catalog/BuildProductCard as the single source of truth for card payloads, reused by home carousels and related products. Drop unused fromArray() from DTOs. Co-authored-by: Cursor <cursoragent@cursor.com>
Set up frontend lint/format (ESLint flat config + Prettier with Tailwind sorting), wire them into composer test-dev and CI, and format resources/js. Type all query closures (Relation eager-loads, Builder taps, model maps) so pest --type-coverage hits 100%. Co-authored-by: Cursor <cursoragent@cursor.com>
Implement home page
Add /categories/{slug} listing (products incl. descendants) with brand,
attribute, price and availability filters, sorting, pagination and
BreadcrumbList JSON-LD. Digikala-style filter UI: accordion sections,
price slider, brand search, per-option counts, instant apply. Attribute
filtering matches via the product_attribute pivot. Thin controller +
Category actions + CategoryDTO + Pest feature tests.
Co-authored-by: Cursor <cursoragent@cursor.com>
Cards now fall back to the first variety image when a product has no featured image (eager-load varieties.image to avoid N+1). AppLink gains a new-tab option and ProductCard opens products in a new tab. Co-authored-by: Cursor <cursoragent@cursor.com>
Update AGENTS.md and STOREFRONT_IMPLEMENTATION.md for the category filter UI, product-card image fallback / new-tab links, and the SSR process restart caveat. Add TAGS.md describing tags as category+attribute SEO landing pages. Co-authored-by: Cursor <cursoragent@cursor.com>
Describe ShopFlow as an open-source single-vendor storefront, the admin/shop monorepo layout, tech stack, domain features, and setup. Co-authored-by: Cursor <cursoragent@cursor.com>
Add /brands/{slug} listing a brand's products with category, price and
availability facets, sorting, pagination and BreadcrumbList JSON-LD.
Thin BrandController + Brand actions + BrandDTO + BrandFilters.vue
(accordion, price slider, category search + counts). Move shared catalog
test helpers to tests/Helpers.php and add BrandPageTest.
Co-authored-by: Cursor <cursoragent@cursor.com>
Serve CMS pages at top-level slugs (/about-us) and FAQ at /faq so the seeded footer links resolve. Seed real about-us/return/privacy/cooperation pages and add Facebook/WhatsApp socials used in the footer. Co-authored-by: Cursor <cursoragent@cursor.com>
Add SitemapController + GetSitemapUrls (home, faq, active categories/brands, published products/pages, excluding no_index) and a dynamic robots.txt. Emit absolute breadcrumb item URLs via a shared seo helper. Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Add keyword search (/search) and autocomplete (/search/suggest) behind a ProductSearch contract (DatabaseProductSearch, ILIKE) so Elasticsearch can swap in later. HeaderSearch shows a debounced dropdown of matching categories and products. Co-authored-by: Cursor <cursoragent@cursor.com>
Add a mobile-first auth flow at /login: identify by mobile, then verify with a one-time code (OTP) or a password. OTP is the primary path and registers the user on first login; password login is an alternative. Codes live in cache via SendOtpCode/VerifyOtpCode and are sent by a logged stub. Resending is blocked until the current code expires, and the remaining seconds drive the UI countdown. OTP sign-ups seed placeholders for the NOT NULL email/password/name columns; blocked users cannot log in. Co-authored-by: Cursor <cursoragent@cursor.com>
Add an auth-only /account section with a shared AccountLayout sidebar, a dashboard overview, and a profile editor (name/email; mobile read-only). Sidebar links not yet built render a placeholder. UserDTO shapes the user payload; profile saves flash a shared status message. Co-authored-by: Cursor <cursoragent@cursor.com>
Add customer addresses under /account/addresses: list, create, edit, set default and soft-delete. Editing is immutable (new row, soft-delete old) and delete is soft-only so order history is preserved; removing the default promotes the newest remaining address. Location is picked on a Neshan map: the interactive web-key map when NESHAN_MAP_KEY is set, otherwise a draggable static-map fallback via the service key (cached, timeout-safe). Selecting a point reverse-geocodes to fill the address. Plate/unit are encoded into the description column; lat/long columns are defined in create_addresses_table. Co-authored-by: Cursor <cursoragent@cursor.com>
Add a web app manifest, app icons and Apple meta tags so the storefront can be installed to the home screen. On iOS Safari (where there is no native install prompt) show a light bottom-sheet with step-by-step instructions; it hides when already installed and snoozes 7 days after dismissal. Co-authored-by: Cursor <cursoragent@cursor.com>
Guest/user cart (carts table) with add/update/remove, header count badge, and merge of the guest cart into the account on login. Product buy box mirrors the cart per variety. Checkout shipping step picks an address (or adds one inline) and a per-destination shipping method, seeded in admin ShippingSeeder. Persian validation messages for the address form. Co-authored-by: Cursor <cursoragent@cursor.com>
Sync admin's CACHE.md with shop's copy, which already tracked this key.
…ing destination The documented orders/order_shippings schema never had an address reference at all, so a paid order had nowhere to record which address it ships to. Add a nullable FK (nullOnDelete, matching the pattern for other reference FKs on this table) to the existing create_orders_table migration, expose it on the model/factory/Filament form, and document it in both apps' schema docs.
Shop's copy had drifted from admin's (missing the Settings entry, variety-image notes, seeder-robustness notes) — replace it with an exact copy so both stay in sync going forward.
Wires up the missing final step of checkout: creating a real order and
charging it through Zarinpal (sandbox mode).
- CreatePendingOrder snapshots the cart + address + shipping method into
one Order (PENDING) and its OrderVariety lines inside a DB transaction.
- RequestZarinpalPayment/VerifyZarinpalPayment talk to Zarinpal's REST v4
API; StartCheckoutPayment opens the payment session and StartPay
redirect, CompleteCheckoutPayment handles the callback (looked up by
authority, never session) and is idempotent against duplicate/refreshed
callbacks.
- DecrementInventoryAndMarkPaid performs the Strategy-A row-locked
inventory decrement (docs/ORDER.md) only after payment is verified.
- ValidateCartStock re-checks live inventory before ever opening a
Zarinpal session, so a customer is never charged for something already
out of stock. The residual race that check can't prevent (two
customers reaching payment for the last unit at once) is handled by
CompleteCheckoutPayment::failPaidButOversold(), which preserves
ref_id/paid_at and a refund-needed message instead of a plain FAILED
transaction that would hide that money needs to go back to the
customer.
- Checkout/Payment.vue now really submits; new Checkout/Confirmation.vue
shows the paid order.
Gateway credentials live in config('services.zarinpal.*')/.env rather
than the admin gateways table, since nothing is seeded there yet.
GetCategoryFilters::attributeGroups() ordered facet groups alphabetically by the internal attribute_groups.name, ignoring the order column that exists specifically to control this — so an admin's configured facet sequence had no effect on the category page's filter sidebar. Order by `order` first, `name` as a tiebreak.
BuildProductDetail mapped descriptive product_attribute values to a
bare {value} with no indication of which attribute group they belonged
to, so ProductSpecs.vue rendered raw values like "پنبه" with nothing
saying it's the material. Eager-load attributes.attributeGroup and pair
each value with its group name; render as a group: value list.
…selector
Two bugs in the product page's variety selector, both rooted in
Product::varieties() having no ORDER BY (Postgres gives no row-order
guarantee without one):
- BuildVariantAxes built each axis's options in whatever order varieties
happened to be processed in, e.g. a Size axis rendering "L, S, XL, M"
instead of any sensible order. attributes has no order column, so sort
by attribute id (creation order) as the best available proxy.
- The primary axis wasn't guaranteed to render first — a variety whose
primary attribute was deleted (attribute_id null) but still carrying a
secondary/pivot attribute could push that secondary axis above the
primary one, contradicting "primary attribute group drives selection".
Add Product::varieties()->orderBy('id') for deterministic fetch order,
sort each axis's options, and always place the primary axis first.
…rder
Both resources have an editable `order` field but no `->defaultSort()`
on their tables, so the list always rendered in insertion/id order
regardless of what an admin configured — the field had no visible
effect. Add `->defaultSort('order')`, matching FaqResource's existing
pattern.
…egory pivot relations AttributeGroup::categories() and Category::attributeGroups() both declared ->withPivot(['as_filter', 'required', 'order']), but attribute_group_category has no order column — calling either relation threw SQLSTATE[42703]: undefined column. Nothing currently calls them, so this was a dormant landmine; remove the phantom column and add a regression test that exercises both relations directly.
Staff saw a Zarinpal-paid order missing from the Order → Receipts tab and mistook it for a bug — they're two intentionally separate payment paths (manual/offline vs online gateway). Both list-page subheadings now say so directly, pointing to the other table.
Drop obsolete version key, pin postgres to 16-alpine, add pgdata volume.
Runs boost:mcp inside the admin container via docker exec, so Claude Code can reach it from the repo root without cd-ing into the container.
Customer-facing order identifier: a random unique 10-digit string (orders.tracking_code), auto-generated in Order's creating model event, shown instead of the sequential id. Searchable/copyable column in the Filament Order table.
… shipping data ShippingLineSeeder deleted shipping_lines before its dependents (shipping_methods/shipping_cities), and CitySeeder deleted cities/ provinces before addresses (which uses SoftDeletes, so even a soft-deleted row still blocks the FK) — both threw a 23503 foreign key violation on a normal db:seed run. Also removed TestSeeder's ShippingLineSeeder/ShippingMethodSeeder/ ShippingCitySeeder calls: they wiped out ShippingSeeder's real, checkout-critical shipping methods and replaced them with 20 random fake ones tied to random specific cities (no nationwide fallback), silently breaking the storefront checkout's shipping-method selection.
…nd returns list
Order history: /account/orders (GetUserOrders, paginated newest-first)
and /account/orders/{order} (reusing BuildOrderDTO/OrderDTO built for
checkout confirmation). Line-items/address/payment-summary markup
extracted from Confirmation.vue into Components/Order/OrderDetail.vue,
shared by both pages.
Pay-again retry (Order::isRetryable + RetryOrderPayment): pays
directly without touching the cart. Re-checks live stock for every
line (all-or-nothing), then resets the SAME order back to PENDING and
opens a fresh Zarinpal session via the new OpenZarinpalSession action
(extracted out of StartCheckoutPayment so both flows share it) rather
than cloning a new order per attempt — a customer who cancels and
retries repeatedly accumulates transactions on one order instead of a
new order row each time.
Returns list: /account/returns reuses the same Orders/Index.vue page
and GetUserOrders, just scoped to OrderStatusEnum::RETURNED with its
own title/empty-state copy.
Customer-facing order number is now order.trackingCode everywhere
(Confirmation.vue, Orders/Index.vue, Orders/Show.vue), never the raw
sequential id.
Docs synced across both apps' ORDER.md/IMPLEMENTATION.md/AGENTS.md,
including a documented (not yet built) plan to expire abandoned
PENDING orders after 15 minutes, and a note that marking an order
RETURNED doesn't restock inventory or track refunds today.
users.mobile is nullable at the schema level (admin/staff accounts have none) but UserDTO::$mobile was a non-nullable string, and User::displayName()'s fallback returned $this->mobile despite its own non-nullable return type — a user with no mobile hitting /account/profile crashed with a TypeError. Made mobile nullable in the DTO and hardened displayName()'s fallback chain (name -> mobile -> email -> generic label).
Receipt: GET /account/orders/{order}/receipt renders a standalone,
chrome-less print page (seller contact from the existing footer
settings, buyer name/mobile/address, itemized lines with per-line
discount, totals) with a print/download button (window.print()). Same
ownership check as the order-show page. Linked from Show.vue via a new
"دانلود رسید" link.
Order detail restyle: new OrderStatusProgress component shows a fill
bar through the order's lifecycle stages (current/next stage labels),
with a distinct banner for CANCELED/RETURNED instead of a bar (no real
shipment-tracking data to show discrete stages). Order header wrapped
in a card with the progress bar underneath; line items show a red
discount tag when a line has one.
Auth-only (shared wishlists table has a unique (user_id, product_id)
and no session_id/guest support, unlike cart). POST
/products/{product}/wishlist (WishlistController@toggle +
ToggleWishlist) checks-then-deletes/creates and redirects back.
Heart toggle lives only in BuyBox.vue on the product detail page;
ProductController@show passes isWishlisted as a sibling Inertia prop
(same pattern as cartItems), not baked into ProductDTO. Deliberately
not added to product cards across listings, to avoid threading a bulk
membership lookup through every card-producing action.
/account/wishlist (AccountController@wishlist + GetUserWishlist) reuses
BuildProductCard's card shape and GetUserOrders' {data, meta}
pagination, rendered by Account/Wishlist/Index.vue with a per-row
remove button. Moved the AccountTest placeholder assertion from the
now-real /account/wishlist to /account/reviews.
Implement category listing
Adds a nullable `rating` (unsignedTinyInteger) to the shared reviews table so the storefront review-submission flow can capture star ratings. Nullable because replies (parent_id set) and admin-entered reviews carry none. Admin model fillable/cast, factory, ReviewResource form select + table column, and en/fa lang updated; db doc + IMPLEMENTATION.md reflect the new column.
… badge
Any logged-in user can submit a review (star picker + heading + text)
via POST /products/{product}/reviews (ReviewController@store +
CreateReview action); it's always created PENDING, so it stays hidden
until an admin approves it — the storefront only ever renders
Review::approved() rows.
Star ratings surfaced: the product header and reviews section show the
real average (ProductDTO::averageRating, computed in BuildProductDetail
over approved reviews), replacing the hardcoded 0; each review shows
its own stars.
Verified-buyer ("خریدار") badge is computed at read time by
FindProductBuyers (reviewer with a PAID/PROCESSING/SHIPPED/DELIVERED
order containing the product; CANCELED/RETURNED excluded via explicit
whereIn), never stored. canReview (is-logged-in) is a sibling Inertia
prop like isWishlisted; the form becomes a login prompt for guests.
Also fixes a latent bug where review authors never displayed (used the
non-existent User->name; now displayName()).
8 feature tests in ReviewTest. Docs updated (STOREFRONT_IMPLEMENTATION,
AGENTS, ShoFlow db doc).
…ss in CI RequestZarinpalPayment returns null when services.zarinpal.merchant_id is empty, so the checkout/retry tests fell through to the failure redirect. They only passed locally because the container's .env happens to set ZARINPAL_MERCHANT_ID; CI has no .env, so all 7 failed. Pin merchant_id + base_url as phpunit <env> values (Zarinpal HTTP is faked anyway) so the tests are deterministic everywhere.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
No description provided.