Fayman — Persian RTL menswear storefront
A commerce template rebuilt into a working Iranian menswear store, from the shop and checkout to the account and the admin.
- Company
- Independent
- Role
- Product designer & developer
- Period
- 2026–
- Type
A commerce template rebuilt into a working Iranian menswear store, from the shop and checkout to the account and the admin.



01Context
Fayman (فیمن) sells tailored menswear online: suits, blazers, trousers, shirts, shoes and accessories, with a size guide, a fabric guide and a made-to-measure service. The store started on 23 June 2026 from the official Payload ecommerce template plus a first clean-up pass. By 15 September, 491 commits later, most of it had been replaced.
The template gave a working catalogue, a cart and an order model. It also assumed Stripe, US dollars, email accounts and hosted tooling. The work was to turn it into a store an Iranian customer can use from the first search to a return request, and that the store’s staff can run without a developer.
Of the 518 source files at the end, 80 are unchanged from the template. Payments, sign-in, checkout and currency were replaced outright; the customer account, the admin tools and 17 of the 25 page blocks are new.

02Problem
Every assumption in an international commerce template had to be replaced, not configured:
03My role
I designed the product and directed its build. AI coding agents wrote most of the code from written specs; scope, design direction and every production release were decided by people.
A raw comment was investigated against the real code before it became a spec an agent could build without asking a follow-up question. Every production release is written into an append-only deploy ledger.
returns to 01 · Report
04System
A clothing store is judged on the path from a search to a delivered order, and on what happens when that path breaks. The parts below share data: the badge on a product card, the shop’s sale filter, the cart total and the amount sent to the bank all come from one pricing rule.
Design ran through a review loop of its own: a screenshot of a problem, a diagnosis against the code and the design language, a card, then a build. By 12 September there were 63 design cards, 55 of them done.
One pricing resolver feeds the product card, the cart, the bill and the payment check, so the price a customer sees is the price the bank is asked for.
05Discovery
Customers reach a suit from different questions: an occasion, a category, a sale, a search or a shared link. The home page answers several of them, and each answer ends in the same shop.
Its product rails come from one block, the Product Section, with four sources: a category rule with sale-only and in-stock switches, hand-picked products, a pasted shop URL, or a list of product links. It has three layouts: rail, grid and spotlight. Every source runs through the shop’s own query, so a rail and the shop can’t disagree about what is on sale or in stock. Editors use it on the live home page today for rails such as discounted full sets and single sizes.
The occasion row sits above the category row, following the store’s feedback that a customer starts from “I have a wedding”, not “I want a blazer”. In the shop, every filter lives in the URL: fabric, occasion, style, a price range in Toman, in stock only and on sale only. Stock, sale and price are checked on the same size, so a sold-out size on sale can’t pull a product into the results. On a phone the filters open as a sheet, and active filters stay visible as removable chips.
Search is the weakest entry point. It matches product titles only and doesn’t yet treat Arabic and Persian letter forms as the same. The lookbook is editorial: its looks hand off to filtered shop pages rather than to single products.


Filters open as a sheet.

An active occasion filter stays as a removable chip, with a count on the button.

Search results are the shop, filtered by the query.
06Product page
Buying tailored clothes online raises three questions: will it fit, what is it made of, and what happens if it’s wrong. The product page was rebuilt around them: a buy column that stays beside the gallery, a stock line, a trust row with the delivery and 7-day return terms, an accordion for size, care and shipping, and a buy bar that stays in reach on a phone.
Sizes had to tell the truth. On 13 August, 23 of the 25 products with sizes offered at least one size that nothing behind it could sell. One shared rule now hides sizes a product isn’t made in, disables sizes that are sold out, and labels a product with nothing left «ناموجود». Below ten pieces, the page says how many are left.
The Size Finder on the size guide takes the garment, a preferred fit and body measurements, and answers from the store’s size tables. Between two sizes it picks the larger one, since taking in is safer than letting out. Out of range it offers a measuring consultation, and for shirts it gives a collar reading instead of guessing a letter size. The fabric guide matches cloth to season and use, and each fabric links to the shop filtered by it.
Two parts are unfinished. The Size Finder isn’t on the product page and doesn’t read what each product actually sells, and the product page doesn’t show each product’s fabric.

Local build, test data: one size sold out and disabled, a size chosen, stock confirmed, the terms beside the button.

The live store: a discounted product with its old price struck through.

The Size Finder: garment, fit and measurements, then a size.

The fabric guide: which cloth suits which season, use and care.

Shipping: methods, times and the free-delivery threshold.

The FAQ, grouped by topic and searchable in Persian.
07Checkout
The cart is priced on the server by the same resolver the payment check uses, so the cart and the bank can’t disagree. Opening the cart re-prices it from the live catalogue: on production, 4 of 15 open carts held a total that had gone stale. The bill shows the list price, the product discounts and the coupon as separate lines.
Checkout is one page. On a phone the order summary comes first. A signed-out customer signs in with a mobile number and a six-digit code without leaving the page, and a new number gets an account. The first saved address is selected. Only the payment methods the order qualifies for are shown, and the button names the amount: «پرداخت … تومان». Anything that can’t be used yet is explained in a sentence instead of a greyed-out control, such as a wallet balance that falls short by a named amount.
Payment runs through one adapter for every Iranian gateway: Zarinpal, Zibal, Torob Pay, SnappPay and DigiPay, plus the store wallet. Before sending anyone to a bank, the server recomputes the total and refuses to charge unless it matches the cart to the Rial. The order is created only after the gateway confirms the payment. A repeated callback can’t create a second order, and a job every ten minutes picks up payments whose callback never arrived.
The adapter’s shape comes from real faults. Over two days in September, DigiPay needed seven fixes: an empty endpoint setting that broke checkout, a request format that made valid credentials look invalid, a return from the gateway that hit a missing route, and an amount unit that had to be confirmed as Rial on a live payment. Each became a rule: DigiPay’s endpoint is a required setting whose empty value hides the gateway, every gateway accepts both kinds of return request, and no amount unit is ever guessed.

The cart: list price, product discount and a coupon on separate lines.

Signed out: the summary first, then sign-in by mobile number.

The six-digit code. No account is needed beforehand.

Signed in: a saved address, the methods, a wallet shortfall in words, and the amount on the button.
08Key decisions
Each one swaps a template default for the constraint it ignored.
Prices are spoken in Toman but settled in Rial. Every amount is stored as whole Rial and converted in one place for display, so a factor-of-ten error can’t reach a charge.
Sign-in, sign-up and checkout take a mobile number and a one-time code. The email and password pages were retired: none could work without an email service, which production never had.

The template confirmed orders from the browser. Here the server verifies the payment with the gateway, including the amount, and only then creates the order and takes the stock.
The wallet plugs into the same adapter as the gateways, so it gets the same total check, duplicate protection and recovery job. Its ledger is append-only: no one, admins included, can edit or delete an entry.
Staff confirm each paid order in the admin. That is a separate flag with a confirm button, not a fifth status, because the status is shown to customers and internal steps shouldn’t reach them.
Code, container images, media storage, deploys and analytics all run in Iran, so a fix can ship when GitHub or Docker Hub is unreachable.
09Account
A returning customer signs in with the same phone number and lands on a dashboard built around what people come back for: orders in progress, the wallet balance, saved products, open returns and the last three orders. The earlier page was a settings form, with the order status the third panel down.
The order list filters by status and shows product thumbnails, because customers recognise an order by what is in it. The order page shows a three-stage track (placed, processing, completed), the prices paid at the time, the address, and a return button for seven days. It shows only what the data supports: the track has no dates for later stages, because the system doesn’t record when each change happened.
A return request picks items, quantities and a reason, and the server checks the owner, the seven-day window and what was already returned. Staff move the request through its statuses. No money moves automatically; staff can add the refund to the customer’s wallet by hand. The wallet page answers “why is my balance this number?” with a statement in which each entry has a reason, a link to its order and the balance after it. There is no top-up button, because top-up doesn’t exist and a button would promise it.
Addresses saved here are the same records checkout uses. The wishlist keeps products for signed-in customers. Settings holds a name and shows the verified phone, which can’t be changed there. Notification preferences are saved, but nothing sends messages from them yet: apart from sign-in codes, customers receive no SMS.

The dashboard: orders in progress, wallet, wishlist, open returns and recent orders.

The wallet statement: a gift credit, an order paid from the wallet and a refund for a return.
10Commerce rules
Discounts have two layers. A product or a category can carry a discount with start and end dates set on a Persian calendar; the product’s own discount wins, and the two never stack. A coupon then applies once per cart, to the already-discounted total, and counts as used only after a confirmed payment. The ٪ badge, the struck-through price, the shop’s sale filter and the sale rails all read the same rule.
That order was learned from a real basket. For six days in August, coupons were applied to the list price and replaced the product discount, which overcharged customers; a weak coupon could even raise the price. Coupons now apply after catalogue discounts.
Stock is kept per size. It is checked when payment starts and taken only after the bank confirms, in one guarded update, so two buyers of the last piece can’t both succeed. If stock runs out between the two, the order is marked refunded, a payment reversal is attempted, and any stock already taken for that order is given back.
Staff manage this from a discount console that shows each discount as active, scheduled, expired, off or used up, and from a generator that makes single-use codes for a campaign.

11Operations
The admin is in Farsi by default, since the staff are in Iran, and discount dates are picked on a Persian calendar. Pages are built from blocks: the template had 8, the store has 25, and 17 are new, from the Product Section to the size guide, the FAQ and the returns policy. Header links, footer columns, social links and the order of categories are edited in the admin.
The operating tools were built for jobs the staff actually do. Paid orders wait for an operator’s confirmation and stay flagged in red until someone confirms them. A size and stock screen shows stock per size across each category and warns before a size that orders still use is deleted. Wallet credit is typed in Rial and spelled out in Toman words under the field, because a credit can’t be undone.
SMS carries three events: the sign-in code, a sale alert to staff, and new corporate-sales leads. Each has an editable template and its own recipients. The corporate-sales page takes enquiries for 50 pieces or more as leads; there is no bulk pricing or quote system.
Two limits remain: there is one admin role, with no narrower staff account, and the size tables behind the Size Finder live in code.
12Built for Iran
Right-to-left went beyond the direction attribute. Layout moved to logical properties, which the template didn’t use at all; carousels and galleries follow the reading direction on the keyboard too; and phone numbers, postal codes and coupon codes sit in left-to-right fields inside Persian forms. Prices use Persian digits, and dates use the Persian calendar through the browser’s own calendar support rather than a library.
The design language was adapted, not invented. An adaptation layer on top of an existing system, written for English software, names where it disagrees with a Persian menswear store and settles that the adaptation wins. Its sharpest rule is zero letter-spacing on Persian, because letter-spacing breaks joined letters. The fonts are self-hosted, so the store never waits on Google.
Money follows local rules: whole Rial stored, Toman shown, and Torob price tags in Toman so the price-comparison site lists the right number. The footer carries the E-namad seal, which customers can click through to Iran’s e-commerce registry. On 27 September the store’s public checkout settings listed Zibal, the wallet, Torob Pay and DigiPay.
The infrastructure stays in the country: code and images on a self-hosted Git server, media on ArvanCloud storage, deploys through Coolify, and analytics on a self-hosted Umami. The architecture notes put the rule plainly: everything the live site depends on at build and deploy time must be reachable in-country.
It is the one item here that produces confidently wrong numbers rather than missing ones.
13Reliability
On 10 August the store reported one product that returned 404 while it still appeared in the admin. A read-only sweep of production showed the real scale: 113 of 166 published products were unreachable. Two bugs were stacked. Payload’s default slug function dropped every Persian letter, and Next.js handed pages their Persian slugs still percent-encoded, so even a correct slug failed. The code was fixed first, then the 113 slugs were repaired on production, and a second sweep returned 200 for all 166.
Releases pass one local gate: generated types and import maps checked, then type checks, lint, tests and a production build. The suite held about 1,086 tests on 1 September, 772 of them unit tests, and 1,354 by 12 September. Every production release is written into an append-only deploy ledger. A smoke script then checks each gateway’s return route, analytics and the shop, and every check in it exists because that fault once reached production.
A dependency sweep on 1 September updated 54 of 58 outdated packages, in two verified batches plus nine major upgrades on their own, and recorded why the other four were held back. No page-speed numbers were ever measured, so none are claimed here; the one measured build change is the compile step, from 88 to 18.2 seconds.
…would have changed nothing a customer sees, since a correct Persian slug still 404’d.
14Outcomes
Live at faymen.ir since 7 July 2026 and taking payments. No commercial figures are published. The numbers below are measured fixes; the rest is delivered scope.
15What I learned
“Docker Hub is unreachable” decides whether a fix can ship during a sale; “there is no email service” rewrote identity, checkout and the newsletter.
A report of one product returning 404 while still visible in the admin turned out, once checked against the code and the catalogue, to be two thirds of the store and two separate bugs.
The order track has no dates because none are recorded; a count that fails to load shows a dash, not zero; the wallet has no top-up button; the Size Finder won’t name a size the shop can’t sell.
Adapting an existing design language was cheaper than writing a new one, and the disagreements, such as Persian letter-spacing and no illustration, became its most useful part.