parity(api,apps): delivery-mode gaps — master OTP, non-blocking SMS, faToEn, v2 cart cap
D20: OTP_MASTER_CODE login bypass read only from the environment, disabled when unset, audited on every use, with a boot banner while armed. SMS no longer aborts the boot in production — it warns and declines silently, so no message can stop an operation. D19: port Utility::faTOen onto admin-entered mobiles (before the uniqueness lookup, which could previously be bypassed with Persian digits), and the create-only welcome SMS, byte-identical to the reference and fire-and-forget. user-app-v2 was missing the cart stock ceiling entirely: capQty/maxQty, the un-orderable guard and the qty clamp are ported, leaving v2's own design intact. The two cart stores are now byte-identical. isVariantOrderable and the orderable-first default selection ported too. jest 45/45; typecheck clean on api, admin-panel, user-app, user-app-v2. [ref: henza_server/app/Http/Controllers/Adm/AdminController.php:333,348,364,378-387] [ref: henza_server/app/Models/Utility.php:26] Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
2194b3d4d9
commit
460cf276b6
@@ -36,7 +36,13 @@
|
||||
not last, to land there. Keep both steppers' DOM order in sync if
|
||||
either one changes. -->
|
||||
<div class="ci__qty">
|
||||
<button type="button" class="ci__qtyBtn" aria-label="افزودن" @click="increment">
|
||||
<button
|
||||
type="button"
|
||||
class="ci__qtyBtn"
|
||||
aria-label="افزودن"
|
||||
:disabled="!canIncrement"
|
||||
@click="increment"
|
||||
>
|
||||
<Icon icon="vuesax-linear:add" size="16" />
|
||||
</button>
|
||||
<span class="ci__qtyValue">{{ value.qty }}</span>
|
||||
@@ -77,7 +83,13 @@ const emit = defineEmits<{ (e: 'note', value: number): void }>()
|
||||
const cart = useCartStore()
|
||||
const index = cart.find_index(props.value)
|
||||
|
||||
// Stock ceiling, same rule as the product page: uncapped for a pre-order
|
||||
// variant, otherwise limited to what is on hand. The store clamps as well.
|
||||
const maxQty = computed(() => cart.maxQty(props.value))
|
||||
const canIncrement = computed(() => maxQty.value === null || props.value.qty < maxQty.value)
|
||||
|
||||
const increment = () => {
|
||||
if (!canIncrement.value) return
|
||||
cart.updateFeild(props.value.qty + 1, index, props.value, 'qty')
|
||||
}
|
||||
const decrement = () => {
|
||||
|
||||
@@ -149,7 +149,8 @@
|
||||
:key="item.id"
|
||||
type="button"
|
||||
class="pd__chip"
|
||||
:class="{ 'is-on': selectedVariant?.id === item.id }"
|
||||
:class="{ 'is-on': selectedVariant?.id === item.id, 'is-out': !isOrderable(item) }"
|
||||
:disabled="!isOrderable(item)"
|
||||
@click="selectedVariant = item"
|
||||
>
|
||||
{{ item.title }}
|
||||
@@ -324,7 +325,7 @@ import GallerySlider from './GallerySlider.vue'
|
||||
import Spinner from '../Spinner.vue'
|
||||
import NotifyVant from '../NotifyVant.vue'
|
||||
import DialogVant from '../DialogVant.vue'
|
||||
import { useProduct } from '../../composables/useProduct'
|
||||
import { useProduct, isVariantOrderable } from '../../composables/useProduct'
|
||||
import { useProductDetail } from '../../composables/useProductDetail'
|
||||
import { useFavoritesStore } from '../../stores/favoritesStore'
|
||||
import { useFiltersStore } from '../../stores/filtersStore'
|
||||
@@ -477,7 +478,14 @@ const inCart = computed(() => cart.isInCart(storeProduct.value))
|
||||
const cartIndex = computed(() => cart.find_index(storeProduct.value))
|
||||
const cartQty = computed(() => (cartIndex.value > -1 ? cart.items[cartIndex.value].qty : 0))
|
||||
|
||||
const increment = () => cart.updateFeild(cartQty.value + 1, cartIndex.value, storeProduct.value, 'qty')
|
||||
// Stock ceiling: a pre-order variant may exceed what is on hand, anything else
|
||||
// stops at its qty. The store clamps too — this just keeps the button honest.
|
||||
const cartMaxQty = computed(() => cart.maxQty(storeProduct.value))
|
||||
const canIncrement = computed(() => cartMaxQty.value === null || cartQty.value < cartMaxQty.value)
|
||||
const increment = () => {
|
||||
if (!canIncrement.value) return
|
||||
cart.updateFeild(cartQty.value + 1, cartIndex.value, storeProduct.value, 'qty')
|
||||
}
|
||||
const decrement = () => {
|
||||
if (cartQty.value <= 1) {
|
||||
cart.remove(storeProduct.value)
|
||||
@@ -486,8 +494,11 @@ const decrement = () => {
|
||||
cart.updateFeild(cartQty.value - 1, cartIndex.value, storeProduct.value, 'qty')
|
||||
}
|
||||
|
||||
const isOrderable = (v: Parameters<typeof isVariantOrderable>[0]) => isVariantOrderable(v)
|
||||
|
||||
const addToCart = () => {
|
||||
cart.addToCart(storeProduct.value)
|
||||
// The store refuses an un-orderable variant; only confirm when it accepted.
|
||||
if (!cart.addToCart(storeProduct.value)) return
|
||||
notify.value = true
|
||||
}
|
||||
|
||||
@@ -1127,6 +1138,12 @@ onBeforeUnmount(() => {
|
||||
background: @ink;
|
||||
border-color: @ink;
|
||||
}
|
||||
/* Same treatment as v1's .pd__variant.is-out: an un-orderable weight stays
|
||||
visible (so the customer can see it exists) but reads as unavailable. */
|
||||
.pd__chip.is-out {
|
||||
opacity: 0.45;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/* ── Blocks ──────────────────────────────────────────────── */
|
||||
.pd__block {
|
||||
|
||||
Reference in New Issue
Block a user