Add پس کرایه checkbox beside shipping price and show it to customers.

Admin can mark a group as freight collect so the amount input is disabled and checkout shows پس کرایه at zero cost.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Alireza Hassani
2026-10-02 22:37:30 +03:30
co-authored by Cursor
parent 41f21999d2
commit 55071af98c
12 changed files with 200 additions and 99 deletions
@@ -57,6 +57,7 @@ function parseOrderShipping(shippingAddress: Record<string, unknown>) {
title: typeof row.title === 'string' ? row.title : '', title: typeof row.title === 'string' ? row.title : '',
methodLabel: typeof row.methodLabel === 'string' ? row.methodLabel : null, methodLabel: typeof row.methodLabel === 'string' ? row.methodLabel : null,
amount: typeof row.amount === 'number' && Number.isFinite(row.amount) ? row.amount : 0, amount: typeof row.amount === 'number' && Number.isFinite(row.amount) ? row.amount : 0,
pasKraye: row.pasKraye === true,
itemNames: Array.isArray(row.itemNames) itemNames: Array.isArray(row.itemNames)
? row.itemNames.filter((name): name is string => typeof name === 'string') ? row.itemNames.filter((name): name is string => typeof name === 'string')
: [], : [],
@@ -75,6 +76,7 @@ function parseOrderShipping(shippingAddress: Record<string, unknown>) {
deliveryMode, deliveryMode,
addressLine: addressParts.join(' · '), addressLine: addressParts.join(' · '),
groups, groups,
hasPasKraye: groups.some((group) => group.pasKraye),
} }
} }
@@ -109,6 +111,7 @@ export function OrderItemsModal({ open, order, onClose }: OrderItemsModalProps)
if (!mounted || !order) return null if (!mounted || !order) return null
const itemCount = totalQuantity(order) const itemCount = totalQuantity(order)
const shippingInfo = parseOrderShipping(order.shippingAddress)
return createPortal( return createPortal(
<div <div
@@ -153,46 +156,43 @@ export function OrderItemsModal({ open, order, onClose }: OrderItemsModalProps)
</span> </span>
</div> </div>
{(() => { <div className={styles.shippingBlock}>
const shippingInfo = parseOrderShipping(order.shippingAddress) <div className={styles.shippingSectionTitle}>{t('orderItems.delivery')}</div>
return ( {shippingInfo.deliveryMode === 'pickup' ? (
<div className={styles.shippingBlock}> <p className={styles.addressText}>{t('orderItems.pickup')}</p>
<div className={styles.shippingSectionTitle}>{t('orderItems.delivery')}</div> ) : shippingInfo.addressLine ? (
{shippingInfo.deliveryMode === 'pickup' ? ( <p className={styles.addressText}>{shippingInfo.addressLine}</p>
<p className={styles.addressText}>{t('orderItems.pickup')}</p> ) : null}
) : shippingInfo.addressLine ? (
<p className={styles.addressText}>{shippingInfo.addressLine}</p>
) : null}
{shippingInfo.groups.length > 0 ? ( {shippingInfo.groups.length > 0 ? (
<ul className={styles.shippingGroupList}> <ul className={styles.shippingGroupList}>
{shippingInfo.groups.map((group) => ( {shippingInfo.groups.map((group) => (
<li key={group.groupId} className={styles.shippingGroupCard}> <li key={group.groupId} className={styles.shippingGroupCard}>
<div className={styles.shippingGroupHeader}> <div className={styles.shippingGroupHeader}>
<span className={styles.shippingGroupTitle}>{group.title}</span> <span className={styles.shippingGroupTitle}>{group.title}</span>
<span className={styles.shippingGroupAmount}> <span className={styles.shippingGroupAmount}>
{group.amount > 0 {group.pasKraye
? formatIrtPrice(group.amount) ? t('orderItems.pasKraye')
: t('orderItems.freeShipping')} : group.amount > 0
</span> ? formatIrtPrice(group.amount)
</div> : t('orderItems.freeShipping')}
{group.methodLabel ? ( </span>
<div className={styles.shippingMethod}>{group.methodLabel}</div> </div>
) : null} {group.methodLabel ? (
{group.itemNames.length > 0 ? ( <div className={styles.shippingMethod}>{group.methodLabel}</div>
<ul className={styles.shippingItemNames}> ) : null}
{group.itemNames.map((name) => ( {group.itemNames.length > 0 ? (
<li key={`${group.groupId}-${name}`}>{name}</li> <ul className={styles.shippingItemNames}>
))} {group.itemNames.map((name) => (
</ul> <li key={`${group.groupId}-${name}`}>{name}</li>
) : null} ))}
</li> </ul>
))} ) : null}
</ul> </li>
) : null} ))}
</div> </ul>
) ) : null}
})()} </div>
{order.items.length === 0 ? ( {order.items.length === 0 ? (
<p className={styles.empty}>{t('orderItems.empty')}</p> <p className={styles.empty}>{t('orderItems.empty')}</p>
@@ -243,7 +243,9 @@ export function OrderItemsModal({ open, order, onClose }: OrderItemsModalProps)
<span className={styles.totalsValue}> <span className={styles.totalsValue}>
{order.shippingTotal > 0 {order.shippingTotal > 0
? formatIrtPrice(order.shippingTotal) ? formatIrtPrice(order.shippingTotal)
: t('orderItems.freeShipping')} : shippingInfo.hasPasKraye
? t('orderItems.pasKraye')
: t('orderItems.freeShipping')}
</span> </span>
</div> </div>
{order.discountTotal > 0 ? ( {order.discountTotal > 0 ? (
@@ -199,6 +199,37 @@
direction: ltr; direction: ltr;
} }
.pasKrayeCheck {
display: inline-flex;
align-items: center;
gap: 6px;
flex-shrink: 0;
margin-inline-start: 4px;
padding: 6px 10px;
border-radius: var(--radius-sm);
border: 1px solid rgba(148, 163, 184, 0.28);
background: rgba(148, 163, 184, 0.08);
font-size: 12px;
font-weight: 700;
color: var(--text-primary);
cursor: pointer;
user-select: none;
white-space: nowrap;
}
.pasKrayeCheck input {
width: 15px;
height: 15px;
accent-color: var(--primary);
cursor: pointer;
}
.amountRow input:disabled {
opacity: 0.55;
cursor: not-allowed;
background: rgba(148, 163, 184, 0.1);
}
.checkRow { .checkRow {
display: flex; display: flex;
align-items: flex-start; align-items: flex-start;
@@ -47,6 +47,7 @@ export function ShippingCostGroupModal({
const [description, setDescription] = useState('') const [description, setDescription] = useState('')
const [methods, setMethods] = useState<ShippingMethodId[]>([]) const [methods, setMethods] = useState<ShippingMethodId[]>([])
const [defaultAmount, setDefaultAmount] = useState('') const [defaultAmount, setDefaultAmount] = useState('')
const [pasKraye, setPasKraye] = useState(false)
const [isDefault, setIsDefault] = useState(false) const [isDefault, setIsDefault] = useState(false)
const [formError, setFormError] = useState('') const [formError, setFormError] = useState('')
@@ -58,6 +59,7 @@ export function ShippingCostGroupModal({
setNameEn(editingGroup?.nameEn ?? '') setNameEn(editingGroup?.nameEn ?? '')
setDescription(editingGroup?.description ?? '') setDescription(editingGroup?.description ?? '')
setMethods(editingGroup?.methods ?? []) setMethods(editingGroup?.methods ?? [])
setPasKraye(editingGroup?.pasKraye ?? false)
setDefaultAmount( setDefaultAmount(
editingGroup editingGroup
? formatIrtInput(String(Math.round(editingGroup.defaultAmount))) ? formatIrtInput(String(Math.round(editingGroup.defaultAmount)))
@@ -98,7 +100,7 @@ export function ShippingCostGroupModal({
const trimmedFa = nameFa.trim() const trimmedFa = nameFa.trim()
const trimmedEn = nameEn.trim() const trimmedEn = nameEn.trim()
const amount = parseIrtInput(defaultAmount) const amount = pasKraye ? 0 : parseIrtInput(defaultAmount)
if (!trimmedFa || !trimmedEn) { if (!trimmedFa || !trimmedEn) {
setFormError(t('store.shipping.form.errorRequired')) setFormError(t('store.shipping.form.errorRequired'))
@@ -108,7 +110,7 @@ export function ShippingCostGroupModal({
setFormError(t('store.shipping.form.errorMethods')) setFormError(t('store.shipping.form.errorMethods'))
return return
} }
if (amount === null || amount < 0) { if (!pasKraye && (amount === null || amount < 0)) {
setFormError(t('store.shipping.form.errorAmount')) setFormError(t('store.shipping.form.errorAmount'))
return return
} }
@@ -118,7 +120,8 @@ export function ShippingCostGroupModal({
nameEn: trimmedEn, nameEn: trimmedEn,
description: description.trim() || undefined, description: description.trim() || undefined,
methods, methods,
defaultAmount: amount, defaultAmount: amount ?? 0,
pasKraye,
isDefault, isDefault,
}) })
} }
@@ -244,11 +247,28 @@ export function ShippingCostGroupModal({
placeholder={t('store.shipping.form.defaultAmountPlaceholder')} placeholder={t('store.shipping.form.defaultAmountPlaceholder')}
value={defaultAmount} value={defaultAmount}
onChange={(e) => setDefaultAmount(formatIrtInput(e.target.value))} onChange={(e) => setDefaultAmount(formatIrtInput(e.target.value))}
disabled={isSubmitting} disabled={isSubmitting || pasKraye}
required required={!pasKraye}
/> />
<span className={styles.amountUnit}>IRT</span> <span className={styles.amountUnit}>IRT</span>
<label className={styles.pasKrayeCheck} htmlFor="shipping-pas-kraye">
<input
id="shipping-pas-kraye"
type="checkbox"
checked={pasKraye}
onChange={(e) => {
const checked = e.target.checked
setPasKraye(checked)
if (checked) setDefaultAmount('')
}}
disabled={isSubmitting}
/>
<span>{t('store.shipping.form.pasKraye')}</span>
</label>
</div> </div>
{pasKraye ? (
<p className={styles.hint}>{t('store.shipping.form.pasKrayeHint')}</p>
) : null}
</div> </div>
<label className={styles.checkRow} htmlFor="shipping-is-default"> <label className={styles.checkRow} htmlFor="shipping-is-default">
@@ -59,7 +59,11 @@ export function ShippingCostGroupRow({
<div className={styles.chip}> <div className={styles.chip}>
<span className={styles.chipLabel}>{t('store.shipping.row.defaultAmount')}</span> <span className={styles.chipLabel}>{t('store.shipping.row.defaultAmount')}</span>
<span className={styles.chipValue}> <span className={styles.chipValue}>
<span className={styles.chipNumber}>{formatIrtPrice(group.defaultAmount)}</span> <span className={styles.chipNumber}>
{group.pasKraye
? t('store.shipping.form.pasKraye')
: formatIrtPrice(group.defaultAmount)}
</span>
</span> </span>
</div> </div>
+8
View File
@@ -1006,6 +1006,9 @@ const en = {
'store.shipping.form.defaultAmountHint': 'store.shipping.form.defaultAmountHint':
'Applied to all provinces unless a city-specific amount is set later.', 'Applied to all provinces unless a city-specific amount is set later.',
'store.shipping.form.defaultAmountPlaceholder': 'e.g. 120,000', 'store.shipping.form.defaultAmountPlaceholder': 'e.g. 120,000',
'store.shipping.form.pasKraye': 'Freight collect',
'store.shipping.form.pasKrayeHint':
'Shipping is free at checkout, but customers see “پس کرایه” (pay on delivery).',
'store.shipping.form.isDefault': 'Make this the default group', 'store.shipping.form.isDefault': 'Make this the default group',
'store.shipping.form.isDefaultHint': 'store.shipping.form.isDefaultHint':
'Categories with no assigned shipping group will use this group.', 'Categories with no assigned shipping group will use this group.',
@@ -1303,6 +1306,7 @@ const en = {
'orderItems.shipping': 'Shipping', 'orderItems.shipping': 'Shipping',
'orderItems.discount': 'Discount', 'orderItems.discount': 'Discount',
'orderItems.freeShipping': 'Free shipping', 'orderItems.freeShipping': 'Free shipping',
'orderItems.pasKraye': 'Freight collect',
'orderTransactions.title': 'Transaction details', 'orderTransactions.title': 'Transaction details',
'orderTransactions.empty': 'No payment transactions recorded for this order.', 'orderTransactions.empty': 'No payment transactions recorded for this order.',
@@ -3896,6 +3900,9 @@ const fa: Record<MessageKey, string> = {
'store.shipping.form.defaultAmountHint': 'store.shipping.form.defaultAmountHint':
'برای همه استان‌ها اعمال می‌شود مگر اینکه برای شهر خاصی مبلغ دیگری تعیین شود.', 'برای همه استان‌ها اعمال می‌شود مگر اینکه برای شهر خاصی مبلغ دیگری تعیین شود.',
'store.shipping.form.defaultAmountPlaceholder': 'مثلاً ۱۲۰,۰۰۰', 'store.shipping.form.defaultAmountPlaceholder': 'مثلاً ۱۲۰,۰۰۰',
'store.shipping.form.pasKraye': 'پس کرایه',
'store.shipping.form.pasKrayeHint':
'هزینه ارسال در تسویه‌حساب رایگان محاسبه می‌شود، اما به مشتری «پس کرایه» نمایش داده می‌شود.',
'store.shipping.form.isDefault': 'این گروه را به‌عنوان گروه پیش‌فرض تنظیم کن', 'store.shipping.form.isDefault': 'این گروه را به‌عنوان گروه پیش‌فرض تنظیم کن',
'store.shipping.form.isDefaultHint': 'store.shipping.form.isDefaultHint':
'دسته‌هایی که گروه ارسال اختصاصی ندارند از این گروه استفاده می‌کنند.', 'دسته‌هایی که گروه ارسال اختصاصی ندارند از این گروه استفاده می‌کنند.',
@@ -4193,6 +4200,7 @@ const fa: Record<MessageKey, string> = {
'orderItems.shipping': 'هزینه ارسال', 'orderItems.shipping': 'هزینه ارسال',
'orderItems.discount': 'تخفیف', 'orderItems.discount': 'تخفیف',
'orderItems.freeShipping': 'ارسال رایگان', 'orderItems.freeShipping': 'ارسال رایگان',
'orderItems.pasKraye': 'پس کرایه',
'orderTransactions.title': 'جزئیات تراکنش', 'orderTransactions.title': 'جزئیات تراکنش',
'orderTransactions.empty': 'تراکنش پرداختی برای این سفارش ثبت نشده است.', 'orderTransactions.empty': 'تراکنش پرداختی برای این سفارش ثبت نشده است.',
@@ -38,6 +38,7 @@ export interface ShippingCostGroup {
sortOrder: number sortOrder: number
isActive: boolean isActive: boolean
isDefault: boolean isDefault: boolean
pasKraye: boolean
categoryIds: string[] categoryIds: string[]
categories: ShippingCostGroupCategory[] categories: ShippingCostGroupCategory[]
cityRates: ShippingCostGroupCityRate[] cityRates: ShippingCostGroupCityRate[]
@@ -52,6 +53,7 @@ export interface ShippingCostGroupFormPayload {
description?: string description?: string
methods: ShippingMethodId[] methods: ShippingMethodId[]
defaultAmount: number defaultAmount: number
pasKraye?: boolean
isDefault?: boolean isDefault?: boolean
categoryIds?: string[] categoryIds?: string[]
provinceExceptions?: Array<{ provinceId: string; amount: number }> provinceExceptions?: Array<{ provinceId: string; amount: number }>
@@ -58,6 +58,7 @@ function parseOrderShipping(shippingAddress: Record<string, unknown>) {
title: typeof row.title === 'string' ? row.title : '', title: typeof row.title === 'string' ? row.title : '',
methodLabel: typeof row.methodLabel === 'string' ? row.methodLabel : null, methodLabel: typeof row.methodLabel === 'string' ? row.methodLabel : null,
amount: typeof row.amount === 'number' && Number.isFinite(row.amount) ? row.amount : 0, amount: typeof row.amount === 'number' && Number.isFinite(row.amount) ? row.amount : 0,
pasKraye: row.pasKraye === true,
itemNames: Array.isArray(row.itemNames) itemNames: Array.isArray(row.itemNames)
? row.itemNames.filter((name): name is string => typeof name === 'string') ? row.itemNames.filter((name): name is string => typeof name === 'string')
: [], : [],
@@ -76,6 +77,7 @@ function parseOrderShipping(shippingAddress: Record<string, unknown>) {
deliveryMode, deliveryMode,
addressLine: addressParts.join(' · '), addressLine: addressParts.join(' · '),
groups, groups,
hasPasKraye: groups.some((group) => group.pasKraye),
} }
} }
@@ -110,6 +112,7 @@ export function OrderItemsModal({ open, order, onClose }: OrderItemsModalProps)
if (!mounted || !order) return null if (!mounted || !order) return null
const itemCount = totalQuantity(order) const itemCount = totalQuantity(order)
const shippingInfo = parseOrderShipping(order.shippingAddress)
return createPortal( return createPortal(
<div <div
@@ -154,46 +157,43 @@ export function OrderItemsModal({ open, order, onClose }: OrderItemsModalProps)
</span> </span>
</div> </div>
{(() => { <div className={styles.shippingBlock}>
const shippingInfo = parseOrderShipping(order.shippingAddress) <div className={styles.shippingSectionTitle}>{t('orderItems.delivery')}</div>
return ( {shippingInfo.deliveryMode === 'pickup' ? (
<div className={styles.shippingBlock}> <p className={styles.addressText}>{t('orderItems.pickup')}</p>
<div className={styles.shippingSectionTitle}>{t('orderItems.delivery')}</div> ) : shippingInfo.addressLine ? (
{shippingInfo.deliveryMode === 'pickup' ? ( <p className={styles.addressText}>{shippingInfo.addressLine}</p>
<p className={styles.addressText}>{t('orderItems.pickup')}</p> ) : null}
) : shippingInfo.addressLine ? (
<p className={styles.addressText}>{shippingInfo.addressLine}</p>
) : null}
{shippingInfo.groups.length > 0 ? ( {shippingInfo.groups.length > 0 ? (
<ul className={styles.shippingGroupList}> <ul className={styles.shippingGroupList}>
{shippingInfo.groups.map((group) => ( {shippingInfo.groups.map((group) => (
<li key={group.groupId} className={styles.shippingGroupCard}> <li key={group.groupId} className={styles.shippingGroupCard}>
<div className={styles.shippingGroupHeader}> <div className={styles.shippingGroupHeader}>
<span className={styles.shippingGroupTitle}>{group.title}</span> <span className={styles.shippingGroupTitle}>{group.title}</span>
<span className={styles.shippingGroupAmount}> <span className={styles.shippingGroupAmount}>
{group.amount > 0 {group.pasKraye
? formatIrtPrice(group.amount) ? t('orderItems.pasKraye')
: t('orderItems.freeShipping')} : group.amount > 0
</span> ? formatIrtPrice(group.amount)
</div> : t('orderItems.freeShipping')}
{group.methodLabel ? ( </span>
<div className={styles.shippingMethod}>{group.methodLabel}</div> </div>
) : null} {group.methodLabel ? (
{group.itemNames.length > 0 ? ( <div className={styles.shippingMethod}>{group.methodLabel}</div>
<ul className={styles.shippingItemNames}> ) : null}
{group.itemNames.map((name) => ( {group.itemNames.length > 0 ? (
<li key={`${group.groupId}-${name}`}>{name}</li> <ul className={styles.shippingItemNames}>
))} {group.itemNames.map((name) => (
</ul> <li key={`${group.groupId}-${name}`}>{name}</li>
) : null} ))}
</li> </ul>
))} ) : null}
</ul> </li>
) : null} ))}
</div> </ul>
) ) : null}
})()} </div>
{order.items.length === 0 ? ( {order.items.length === 0 ? (
<EmptyState layout="inline" message={t('orderItems.empty')} /> <EmptyState layout="inline" message={t('orderItems.empty')} />
@@ -244,7 +244,9 @@ export function OrderItemsModal({ open, order, onClose }: OrderItemsModalProps)
<span className={styles.totalsValue}> <span className={styles.totalsValue}>
{order.shippingTotal > 0 {order.shippingTotal > 0
? formatIrtPrice(order.shippingTotal) ? formatIrtPrice(order.shippingTotal)
: t('orderItems.freeShipping')} : shippingInfo.hasPasKraye
? t('orderItems.pasKraye')
: t('orderItems.freeShipping')}
</span> </span>
</div> </div>
{order.discountTotal > 0 ? ( {order.discountTotal > 0 ? (
@@ -21,6 +21,7 @@ export interface CheckoutShippingQuote {
methodId: string | null methodId: string | null
methodLabel: string | null methodLabel: string | null
amount: number amount: number
pasKraye?: boolean
itemNames: string[] itemNames: string[]
} }
@@ -72,6 +73,7 @@ function parseShippingQuotes(value: unknown): CheckoutShippingQuote[] {
methodId: typeof row.methodId === 'string' ? row.methodId : null, methodId: typeof row.methodId === 'string' ? row.methodId : null,
methodLabel: typeof row.methodLabel === 'string' ? row.methodLabel : null, methodLabel: typeof row.methodLabel === 'string' ? row.methodLabel : null,
amount: typeof row.amount === 'number' && Number.isFinite(row.amount) ? row.amount : 0, amount: typeof row.amount === 'number' && Number.isFinite(row.amount) ? row.amount : 0,
pasKraye: row.pasKraye === true,
itemNames: Array.isArray(row.itemNames) itemNames: Array.isArray(row.itemNames)
? row.itemNames.filter((name): name is string => typeof name === 'string') ? row.itemNames.filter((name): name is string => typeof name === 'string')
: [], : [],
+2
View File
@@ -160,6 +160,7 @@ const en = {
'orderItems.shipping': 'Shipping', 'orderItems.shipping': 'Shipping',
'orderItems.discount': 'Discount', 'orderItems.discount': 'Discount',
'orderItems.freeShipping': 'Free shipping', 'orderItems.freeShipping': 'Free shipping',
'orderItems.pasKraye': 'Freight collect',
'favorites.title': 'My Favorites', 'favorites.title': 'My Favorites',
'favorites.subtitle': 'Products you have saved for later.', 'favorites.subtitle': 'Products you have saved for later.',
@@ -588,6 +589,7 @@ const fa: Record<MessageKey, string> = {
'orderItems.shipping': 'هزینه ارسال', 'orderItems.shipping': 'هزینه ارسال',
'orderItems.discount': 'تخفیف', 'orderItems.discount': 'تخفیف',
'orderItems.freeShipping': 'ارسال رایگان', 'orderItems.freeShipping': 'ارسال رایگان',
'orderItems.pasKraye': 'پس کرایه',
'favorites.title': 'علاقه‌مندی‌ها', 'favorites.title': 'علاقه‌مندی‌ها',
'favorites.subtitle': 'محصولاتی که برای بعد ذخیره کرده‌اید.', 'favorites.subtitle': 'محصولاتی که برای بعد ذخیره کرده‌اید.',
@@ -33,9 +33,16 @@ interface ShippingGroupQuote {
title: string title: string
methods: ShippingMethodId[] methods: ShippingMethodId[]
amount: number amount: number
pasKraye: boolean
items: CartItem[] items: CartItem[]
} }
function formatShippingAmountLabel(quote: { amount: number; pasKraye?: boolean }) {
if (quote.pasKraye) return 'پس کرایه'
if (quote.amount > 0) return formatIrtPrice(quote.amount)
return 'ارسال رایگان'
}
function resolveProvinceId( function resolveProvinceId(
provinces: CityOption[], provinces: CityOption[],
provinceName: string | null | undefined, provinceName: string | null | undefined,
@@ -162,6 +169,7 @@ export function CheckoutDeliveryStep() {
title: STORE_SHIPPING_TITLE, title: STORE_SHIPPING_TITLE,
methods: [], methods: [],
amount: 0, amount: 0,
pasKraye: false,
items: cartItems, items: cartItems,
}, },
] ]
@@ -178,6 +186,7 @@ export function CheckoutDeliveryStep() {
shippingConfig.staticShippingProvinceExceptions, shippingConfig.staticShippingProvinceExceptions,
provinceId, provinceId,
), ),
pasKraye: false,
items: cartItems, items: cartItems,
}, },
] ]
@@ -203,15 +212,19 @@ export function CheckoutDeliveryStep() {
if (existing) { if (existing) {
existing.items.push(item) existing.items.push(item)
} else { } else {
const pasKraye = group.pasKraye === true
buckets.set(group.id, { buckets.set(group.id, {
key: group.id, key: group.id,
title: quoteTitleForGroup(group.isDefault, group.nameFa, group.nameEn), title: quoteTitleForGroup(group.isDefault, group.nameFa, group.nameEn),
methods: group.methods, methods: group.methods,
amount: resolveProvinceAmount( amount: pasKraye
group.defaultAmount, ? 0
group.provinceExceptions, : resolveProvinceAmount(
provinceId, group.defaultAmount,
), group.provinceExceptions,
provinceId,
),
pasKraye,
items: [item], items: [item],
}) })
} }
@@ -223,6 +236,7 @@ export function CheckoutDeliveryStep() {
title: STORE_SHIPPING_TITLE, title: STORE_SHIPPING_TITLE,
methods: [], methods: [],
amount: 0, amount: 0,
pasKraye: false,
items: ungrouped, items: ungrouped,
}) })
} }
@@ -276,6 +290,7 @@ export function CheckoutDeliveryStep() {
? SHIPPING_METHOD_LABELS_FA[methodId as ShippingMethodId] ?? methodId ? SHIPPING_METHOD_LABELS_FA[methodId as ShippingMethodId] ?? methodId
: null, : null,
amount: quote.amount, amount: quote.amount,
pasKraye: quote.pasKraye,
itemNames: quote.items.map( itemNames: quote.items.map(
(item) => item.productNameFa || item.productTitle, (item) => item.productNameFa || item.productTitle,
), ),
@@ -288,6 +303,10 @@ export function CheckoutDeliveryStep() {
() => shippingQuotes.reduce((sum, quote) => sum + quote.amount, 0), () => shippingQuotes.reduce((sum, quote) => sum + quote.amount, 0),
[shippingQuotes], [shippingQuotes],
) )
const hasPasKraye = useMemo(
() => shippingQuotes.some((quote) => quote.pasKraye),
[shippingQuotes],
)
const shippingReady = const shippingReady =
deliveryMode === 'pickup' || deliveryMode === 'pickup' ||
@@ -441,14 +460,14 @@ export function CheckoutDeliveryStep() {
<span <span
className={[ className={[
styles.shippingGroupAmount, styles.shippingGroupAmount,
quote.amount > 0 ? '' : styles.shippingGroupAmountFree, quote.pasKraye || quote.amount <= 0
? styles.shippingGroupAmountFree
: '',
] ]
.filter(Boolean) .filter(Boolean)
.join(' ')} .join(' ')}
> >
{quote.amount > 0 {formatShippingAmountLabel(quote)}
? formatIrtPrice(quote.amount)
: 'ارسال رایگان'}
</span> </span>
</div> </div>
@@ -495,7 +514,13 @@ export function CheckoutDeliveryStep() {
{shippingQuotes.length > 1 ? ( {shippingQuotes.length > 1 ? (
<div className={styles.shippingTotalRow}> <div className={styles.shippingTotalRow}>
<span>جمع هزینه ارسال</span> <span>جمع هزینه ارسال</span>
<span className={styles.priceEn}>{formatIrtPrice(shippingTotal)}</span> <span className={styles.priceEn}>
{hasPasKraye && shippingTotal <= 0
? 'پس کرایه'
: shippingTotal <= 0
? 'ارسال رایگان'
: formatIrtPrice(shippingTotal)}
</span>
</div> </div>
) : null} ) : null}
</> </>
@@ -379,9 +379,11 @@ export function CheckoutPaymentStep() {
<span className={styles.priceEn}> <span className={styles.priceEn}>
{deliveryMode === 'pickup' {deliveryMode === 'pickup'
? '—' ? '—'
: shippingTotal <= 0 : shippingQuotes.some((quote) => quote.pasKraye) && shippingTotal <= 0
? 'ارسال رایگان' ? 'پس کرایه'
: formatIrtPrice(shippingTotal)} : shippingTotal <= 0
? 'ارسال رایگان'
: formatIrtPrice(shippingTotal)}
</span> </span>
</div> </div>
<div className={styles.summaryRow}> <div className={styles.summaryRow}>
@@ -26,6 +26,7 @@ export interface PublicShippingCostGroup {
description: string | null description: string | null
methods: ShippingMethodId[] methods: ShippingMethodId[]
defaultAmount: number defaultAmount: number
pasKraye?: boolean
isDefault: boolean isDefault: boolean
categoryIds: string[] categoryIds: string[]
provinceExceptions?: Array<{ provinceId: string; amount: number }> provinceExceptions?: Array<{ provinceId: string; amount: number }>