feat: catalog read cache (api + user apps) and clearer Zarrin accounting errors

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
s.esmaeili
2026-10-11 14:31:32 +03:30
co-authored by Cursor
parent 561addc676
commit 574fa1b4aa
53 changed files with 1241 additions and 401 deletions
@@ -1,39 +1,16 @@
// The real GET /customers/:id/orders endpoint returns OrderItem rows with
// only `productVariantId` (no nested product/variant title) — this joins
// against the already-existing GET /catalog/products list client-side rather
// against the shared catalogue (stores/catalogStore.ts) client-side rather
// than touching apps/api to add an include.
import { ref } from 'vue'
import { api } from '../api/client'
import { useCatalogStore, type VariantLookup } from '../stores/catalogStore'
export interface VariantLookup {
productTitle: string
variantTitle: string
imageUrl?: string
}
const cache = ref<Map<string, VariantLookup> | null>(null)
async function ensureLoaded() {
if (cache.value) return cache.value
const products = await api.listProducts()
const map = new Map<string, VariantLookup>()
for (const product of products) {
for (const variant of product.variants) {
map.set(variant.id, {
productTitle: product.title,
variantTitle: variant.title,
imageUrl: product.images[0]?.url,
})
}
}
cache.value = map
return map
}
export type { VariantLookup }
export function useOrderItemProduct() {
const catalog = useCatalogStore()
const lookup = async (productVariantId: string): Promise<VariantLookup | undefined> => {
const map = await ensureLoaded()
return map.get(productVariantId)
if (!catalog.loaded) await catalog.load()
return catalog.variantLookup.get(productVariantId)
}
return { lookup }
}
@@ -1,25 +1,26 @@
// Adapted from legacy singleProduct.vue's own fetch (GET /product/:id).
// The new API has no single-product-detail endpoint (only GET
// /catalog/products, the flat list) — apps/api is out of scope for this port,
// so this fetches the full list and finds the matching product client-side.
// Wasteful for a large catalog, but honest: no fabricated endpoint, no silent
// behavior change.
// /catalog/products, the flat list), so the product is found in the shared
// catalogue (stores/catalogStore.ts). When the catalogue is already cached,
// opening a product costs no request at all.
//
// `initial` seeds the page with the ProductSummary the tapped card already
// holds — the same shape, from the same endpoint — so the detail view renders
// complete on its first frame instead of mounting blank and jumping when the
// fetch lands. The fetch still runs, silently, and only replaces the product
// if the server's copy actually differs (stock moved since the list loaded).
// fetch lands. The lookup still runs, silently, and only replaces the product
// if the catalogue's copy actually differs (stock moved since the list loaded).
//
// The silent refresh waits until the opening morph (ProductMorph.vue, ≤420ms)
// is over: the response is the whole catalogue, and parsing it — or swapping
// the product and re-rendering — mid-animation drops frames.
// is over: if the catalogue has to come from the server, parsing it — or
// swapping the product and re-rendering — mid-animation drops frames.
import { ref } from 'vue'
import { api, type ProductSummary } from '../api/client'
import type { ProductSummary } from '../api/client'
import { useCatalogStore } from '../stores/catalogStore'
const SEEDED_REFRESH_DELAY_MS = 600
export function useProductDetail(id: string, initial?: ProductSummary | null) {
const catalog = useCatalogStore()
const loading = ref(false)
const flag = ref(Boolean(initial))
const notFound = ref(false)
@@ -30,7 +31,7 @@ export function useProductDetail(id: string, initial?: ProductSummary | null) {
if (seeded) await new Promise((resolve) => setTimeout(resolve, SEEDED_REFRESH_DELAY_MS))
else loading.value = true
try {
const products = await api.listProducts()
const products = await catalog.load()
const found = products.find((p) => p.id === id) ?? null
if (seeded) {
if (found && JSON.stringify(found) !== JSON.stringify(productRef.value)) productRef.value = found
+21 -10
View File
@@ -1,12 +1,13 @@
// Adapted from legacy henza_pwa/src/Mixins/getProducts.ts. Legacy paginated
// against POST /get-products (page/limit/count). The new API's GET
// /catalog/products returns the full active catalog in one flat response with
// no pagination — so `pull()` (legacy's infinite-scroll "next page" trigger)
// is a no-op after the first successful fetch here, and `totalPages` is
// always 0. This keeps the ScrollFetchSearch infinite-scroll wiring intact
// (it still calls fetch on scroll) without fabricating fake pages.
import { reactive } from 'vue'
import { api, type ProductSummary } from '../api/client'
// no pagination — so `isPull` (legacy's infinite-scroll "next page" trigger)
// is a no-op here, and `totalPages` is always 0. A caller that wants new
// products at the end of the list passes `force` to refresh the cached
// catalogue instead of fabricating fake pages.
import { reactive, watch } from 'vue'
import type { ProductSummary } from '../api/client'
import { useCatalogStore } from '../stores/catalogStore'
export interface ProductsData {
totalPages: number
@@ -15,17 +16,27 @@ export interface ProductsData {
}
export function useProducts() {
const catalog = useCatalogStore()
const productsData: ProductsData = reactive({
totalPages: 0,
productList: [],
productList: catalog.products,
flag: false,
})
const getProducts = async (loading: { value: boolean }, isPull = false) => {
// A refresh from another screen (pull-to-refresh) lands here too.
watch(
() => catalog.products,
(list) => {
productsData.productList = list
productsData.flag = list.length < 1
},
)
const getProducts = async (loading: { value: boolean }, isPull = false, force = false) => {
if (isPull) return // no further pages exist server-side yet
loading.value = true
loading.value = force || !catalog.loaded
try {
productsData.productList = await api.listProducts()
productsData.productList = await catalog.load(force)
productsData.flag = productsData.productList.length < 1
} catch (e) {
console.log(e)