feat(seller): inline quick-edit for product & variant prices

Sellers can tap a price on the products page to edit it inline (Enter to
save / Escape to cancel), mirroring the existing stock quick-edit.

- new reusable InlinePriceEditor (optimistic display, Persian/Latin digit
  input, thousands grouping, loading + toast feedback, a11y labels)
- base price editable on each product row (mobile + desktop column)
- per-variant price editable beside stock in the expanded panel; empty
  clears the override and inherits the base price ("پیش‌فرض: <base>")
- useUpdateProductBasePrice + useUpdateVariantPrice hooks; extend generated
  API client + types for the two new endpoints

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Arda Samadi 2026-07-09 12:41:04 +03:30
parent 16389fdd85
commit 5c8e19001c
5 changed files with 665 additions and 69 deletions

View File

@ -0,0 +1,223 @@
import { useEffect, useRef, useState } from "react";
import { Check, Loader2, Pencil, X } from "lucide-react";
import { cn } from "../../lib/utils";
/* ---------- digit helpers (accept both Persian & Latin input) ---------- */
const FA_DIGITS = "۰۱۲۳۴۵۶۷۸۹";
const AR_DIGITS = "٠١٢٣٤٥٦٧٨٩";
function toLatinDigits(input: string): string {
return input
.replace(/[۰-۹]/g, (d) => String(FA_DIGITS.indexOf(d)))
.replace(/[٠-٩]/g, (d) => String(AR_DIGITS.indexOf(d)));
}
/** Extract only the integer digits from any incoming value ("120000.00" -> "120000"). */
function normalizeIncoming(raw: string | null | undefined): string {
if (raw === null || raw === undefined || raw === "") return "";
const n = parseFloat(toLatinDigits(String(raw)));
if (!isFinite(n) || n <= 0) return "";
return String(Math.round(n));
}
/** Keep only digits typed by the user (drops separators, letters, Persian digits normalized). */
function onlyDigits(input: string): string {
return toLatinDigits(input).replace(/[^\d]/g, "");
}
/** Group with thousands separators for the edit input ("120000" -> "120,000"). */
function groupLatin(digits: string): string {
if (!digits) return "";
return digits.replace(/\B(?=(\d{3})+(?!\d))/g, ",");
}
/** Persian-grouped display ("120000" -> "۱۲۰٬۰۰۰"). */
function toFaDisplay(digits: string): string {
if (!digits) return "";
return Number(digits).toLocaleString("fa-IR");
}
/* ------------------------------- component ------------------------------ */
interface InlinePriceEditorProps {
/** Current raw price (decimal string) or null/"" when unset. */
value: string | null | undefined;
/** Called with the new integer price string, or null when cleared (only if allowEmpty). */
onSave: (value: string | null) => void;
isPending?: boolean;
/** Allow saving an empty value as null (e.g. a variant inheriting the base price). */
allowEmpty?: boolean;
/** Muted hint shown when there is no value and we're not editing (e.g. inherited price). */
placeholder?: string;
suffix?: string;
size?: "sm" | "md";
align?: "start" | "center" | "end";
ariaLabel?: string;
className?: string;
}
export function InlinePriceEditor({
value,
onSave,
isPending = false,
allowEmpty = false,
placeholder,
suffix = "تومان",
size = "md",
align = "end",
ariaLabel = "ویرایش قیمت",
className,
}: InlinePriceEditorProps) {
const [editing, setEditing] = useState(false);
const [draft, setDraft] = useState(""); // raw digits while editing
// Optimistic value shown during the async save so the number doesn't flicker
// back to the old server value before the refetch lands.
const [optimistic, setOptimistic] = useState<string | null>(null);
const inputRef = useRef<HTMLInputElement>(null);
const current = normalizeIncoming(value);
// Clear the optimistic value once the mutation settles.
useEffect(() => {
if (!isPending) setOptimistic(null);
}, [isPending]);
useEffect(() => {
if (editing && inputRef.current) {
inputRef.current.focus();
inputRef.current.select();
}
}, [editing]);
const openEditor = () => {
if (isPending) return;
setDraft(current);
setEditing(true);
};
const commit = () => {
const next = onlyDigits(draft);
// No-op if unchanged.
if (next === current) {
setEditing(false);
return;
}
if (!next) {
if (allowEmpty) {
setOptimistic("");
onSave(null);
} else {
// Empty not allowed (e.g. base price) → discard.
setEditing(false);
return;
}
} else {
setOptimistic(next);
onSave(next);
}
setEditing(false);
};
const cancel = () => {
setDraft(current);
setEditing(false);
};
const sizeText = size === "sm" ? "text-[13px]" : "text-[14px]";
const alignClass =
align === "center"
? "justify-center text-center"
: align === "start"
? "justify-start text-right"
: "justify-end text-right";
/* ------------------------------ edit mode ----------------------------- */
if (editing) {
return (
<div
className={cn("flex items-center gap-1.5", alignClass, className)}
dir="ltr"
>
<div className="relative flex items-center">
<input
ref={inputRef}
type="text"
inputMode="numeric"
aria-label={ariaLabel}
value={groupLatin(draft)}
onChange={(e) => setDraft(onlyDigits(e.target.value))}
onKeyDown={(e) => {
if (e.key === "Enter") {
e.preventDefault();
commit();
} else if (e.key === "Escape") {
e.preventDefault();
cancel();
}
}}
placeholder={allowEmpty ? "پیش‌فرض" : "قیمت"}
className={cn(
"w-24 rounded-lg border border-VITROWN_BLUE bg-WHITE px-2 py-2 text-left tabular-nums outline-none",
"focus:ring-2 focus:ring-VITROWN_BLUE/30 transition-shadow",
sizeText
)}
/>
</div>
<button
type="button"
onClick={commit}
aria-label="ذخیره قیمت"
className="flex h-9 w-9 items-center justify-center rounded-lg bg-VITROWN_BLUE text-WHITE transition-colors hover:opacity-90 cursor-pointer"
>
<Check className="h-4 w-4" />
</button>
<button
type="button"
onClick={cancel}
aria-label="انصراف"
className="flex h-9 w-9 items-center justify-center rounded-lg bg-WHITE3 text-BLACK2 transition-colors hover:bg-GRAY3 cursor-pointer"
>
<X className="h-4 w-4" />
</button>
</div>
);
}
/* ---------------------------- display mode ---------------------------- */
const shown = isPending && optimistic !== null ? optimistic : current;
const hasValue = shown !== "";
return (
<button
type="button"
onClick={openEditor}
disabled={isPending}
aria-label={ariaLabel}
className={cn(
"group inline-flex items-center gap-1.5 rounded-lg px-2 py-1.5 min-h-[36px]",
"transition-colors hover:bg-VITROWN_BLUE/5 cursor-pointer disabled:cursor-default",
alignClass,
className
)}
>
{isPending ? (
<Loader2 className="h-3.5 w-3.5 shrink-0 animate-spin text-VITROWN_BLUE" />
) : (
<Pencil className="h-3.5 w-3.5 shrink-0 text-GRAY opacity-0 transition-opacity group-hover:opacity-100" />
)}
{hasValue ? (
<span className={cn("font-semibold tabular-nums", sizeText)}>
{toFaDisplay(shown)}
<span className="mr-1 text-GRAY font-normal">{suffix}</span>
</span>
) : (
<span className={cn("text-GRAY", sizeText)}>
{placeholder ?? "افزودن قیمت"}
</span>
)}
</button>
);
}
export default InlinePriceEditor;

View File

@ -14,6 +14,10 @@ import {
SellerProductListItem,
VariantStockUpdateRequest,
VariantStockUpdateResponse,
ProductBasePriceUpdateRequest,
ProductBasePriceUpdateResponse,
VariantPriceUpdateRequest,
VariantPriceUpdateResponse,
PaginatedSimilarProduct,
} from "../../src/api/types";
import { useToast } from "../hooks/use-toast";
@ -366,6 +370,107 @@ export const useUpdateVariantStock = () => {
});
};
/**
* Custom hook to update a product's base price (quick edit).
*/
export const useUpdateProductBasePrice = () => {
const token = useAuthToken();
const queryClient = useQueryClient();
const { toast } = useToast();
return useMutation({
mutationFn: async ({
productId,
basePrice,
}: {
productId: string;
basePrice: string;
}): Promise<ProductBasePriceUpdateResponse> => {
if (!token) throw new Error("Authentication required");
const productApi = createProductManagementApi(token);
const data: ProductBasePriceUpdateRequest = {
productId,
basePrice,
};
return await productApi.apiProductV1ProductsBasePricePartialUpdate({
data,
});
},
onSuccess: (data) => {
toast({
title: "موفق",
description: "قیمت با موفقیت به‌روزرسانی شد",
variant: "default",
});
queryClient.invalidateQueries({ queryKey: ["sellerProductsList"] });
queryClient.invalidateQueries({ queryKey: ["products"] });
if (data.id) {
queryClient.invalidateQueries({ queryKey: ["product", data.id] });
}
},
onError: (error: Error) => {
console.error("Base price update failed:", error);
toast({
title: "خطا",
description: "خطا در به‌روزرسانی قیمت. لطفاً دوباره تلاش کنید",
variant: "destructive",
});
},
});
};
/**
* Custom hook to update a single variant's price override (quick edit).
* Pass `price: null` to clear the override and inherit the base price.
*/
export const useUpdateVariantPrice = () => {
const token = useAuthToken();
const queryClient = useQueryClient();
const { toast } = useToast();
return useMutation({
mutationFn: async ({
variantId,
price,
}: {
variantId: string;
price: string | null;
}): Promise<VariantPriceUpdateResponse> => {
if (!token) throw new Error("Authentication required");
const productApi = createProductManagementApi(token);
const data: VariantPriceUpdateRequest = {
variantId,
price,
};
return await productApi.apiProductV1ProductsVariantsPricePartialUpdate({
data,
});
},
onSuccess: (data) => {
toast({
title: "موفق",
description: "قیمت با موفقیت به‌روزرسانی شد",
variant: "default",
});
queryClient.invalidateQueries({ queryKey: ["sellerProductsList"] });
queryClient.invalidateQueries({ queryKey: ["products"] });
if (data.product) {
queryClient.invalidateQueries({ queryKey: ["product", data.product] });
}
},
onError: (error: Error) => {
console.error("Variant price update failed:", error);
toast({
title: "خطا",
description: "خطا در به‌روزرسانی قیمت. لطفاً دوباره تلاش کنید",
variant: "destructive",
});
},
});
};
/**
* Custom hook to fetch similar products for a given product ID
*/

View File

@ -4,6 +4,8 @@ import { useNavigate, useParams } from "@remix-run/react";
import {
useGetSellerProductsList,
useUpdateVariantStock,
useUpdateProductBasePrice,
useUpdateVariantPrice,
} from "../requestHandler/use-product-hooks";
import { cn } from "../lib/utils";
import { SellerProductListItem } from "../../src/api/types";
@ -11,6 +13,15 @@ import placeholderImage from "~/assets/icons/product.png";
import placeholderImageDark from "~/assets/icons/product-dark.png";
import { useRootData } from "~/hooks/use-root-data";
import { ThrottledQuantityControl } from "../components/cart/ThrottledQuantityControl";
import { InlinePriceEditor } from "../components/seller/InlinePriceEditor";
/** Format a raw decimal price string to Persian digits ("120000.00" -> "۱۲۰٬۰۰۰"). */
function faPrice(raw: string | null | undefined): string {
if (raw === null || raw === undefined || raw === "") return "";
const n = parseFloat(raw);
if (!isFinite(n)) return "";
return Math.round(n).toLocaleString("fa-IR");
}
function ProductAccordionItem({
product,
@ -28,11 +39,16 @@ function ProductAccordionItem({
const navigate = useNavigate();
const { theme } = useRootData();
const updateVariantStock = useUpdateVariantStock();
const updateBasePrice = useUpdateProductBasePrice();
const updateVariantPrice = useUpdateVariantPrice();
const [updatingVariantId, setUpdatingVariantId] = useState<string | null>(
null
);
const [pricingVariantId, setPricingVariantId] = useState<string | null>(null);
// Use real variants from API
const variants = product.variants || [];
const productId = product.id || "";
const faBase = faPrice(product.basePrice);
const handleEditProduct = (e: React.MouseEvent) => {
e.stopPropagation();
@ -47,43 +63,64 @@ function ProductAccordionItem({
isSticky && isOpen && "sticky top-0 z-10 bg-WHITE shadow-md"
)}
>
<button
onClick={onToggle}
className="w-full px-4 py-3 flex items-center justify-between hover:bg-WHITE3 transition-colors"
>
<div className="flex items-center gap-4 flex-1 min-w-0">
{product.primaryImage && (
<img
src={
product.primaryImage ||
(theme === "dark" ? placeholderImageDark : placeholderImage)
}
alt={product.title}
onError={(e) => {
e.currentTarget.src =
theme === "dark" ? placeholderImageDark : placeholderImage;
}}
className="w-12 h-12 lg:w-14 lg:h-14 object-contain rounded"
/>
)}
<div className="text-right min-w-0">
<div className="w-full px-4 py-3 flex items-center gap-2 hover:bg-WHITE3 transition-colors">
{/* Image toggles the accordion */}
<button
onClick={onToggle}
aria-label={isOpen ? "بستن" : "نمایش تنوع‌ها"}
className="shrink-0 cursor-pointer"
>
<img
src={
product.primaryImage ||
(theme === "dark" ? placeholderImageDark : placeholderImage)
}
alt={product.title}
onError={(e) => {
e.currentTarget.src =
theme === "dark" ? placeholderImageDark : placeholderImage;
}}
className="w-12 h-12 lg:w-14 lg:h-14 object-contain rounded"
/>
</button>
{/* Title toggles; price editor sits under it on mobile (not inside a button) */}
<div className="flex-1 min-w-0">
<button
onClick={onToggle}
className="block w-full text-right cursor-pointer"
>
<h3 className="font-medium text-sm lg:text-[15px] truncate">
{product.title}
</h3>
{/* Price shown inline on mobile only; desktop uses its own column */}
{product.basePrice && (
<p className="text-xs text-gray-600 mt-1 lg:hidden">
{parseInt(product.basePrice).toLocaleString("fa-IR")} تومان
</p>
)}
</button>
<div className="mt-0.5 lg:hidden">
<InlinePriceEditor
value={product.basePrice}
onSave={(v) => {
if (v) updateBasePrice.mutate({ productId, basePrice: v });
}}
isPending={updateBasePrice.isPending}
size="sm"
align="start"
ariaLabel="ویرایش قیمت محصول"
className="-mr-2"
/>
</div>
</div>
{/* Desktop price column */}
<span className="hidden lg:block w-32 text-center text-[14px] font-semibold">
{product.basePrice
? `${parseInt(product.basePrice).toLocaleString("fa-IR")} تومان`
: "—"}
</span>
<div className="hidden lg:flex w-48 justify-center">
<InlinePriceEditor
value={product.basePrice}
onSave={(v) => {
if (v) updateBasePrice.mutate({ productId, basePrice: v });
}}
isPending={updateBasePrice.isPending}
ariaLabel="ویرایش قیمت محصول"
/>
</div>
{/* Desktop variants column */}
<span className="hidden lg:flex w-24 justify-center">
{variants && variants.length > 0 ? (
@ -94,7 +131,8 @@ function ProductAccordionItem({
<span className="text-GRAY"></span>
)}
</span>
<div className="flex items-center gap-2 lg:w-28 lg:justify-center">
<div className="flex items-center gap-1 lg:w-28 lg:justify-center shrink-0">
{variants && variants.length > 0 && (
<span className="text-xs text-gray-500 bg-gray-100 px-2 py-1 rounded lg:hidden">
{variants.length} نوع
@ -102,18 +140,25 @@ function ProductAccordionItem({
)}
<button
onClick={handleEditProduct}
className="p-1.5 hover:bg-gray-200 rounded-lg transition-colors"
className="p-1.5 hover:bg-gray-200 rounded-lg transition-colors cursor-pointer"
title="ویرایش محصول"
aria-label="ویرایش محصول"
>
<Edit className="w-4 h-4 text-gray-600" />
</button>
{isOpen ? (
<ChevronUp className="w-5 h-5 text-gray-400" />
) : (
<ChevronDown className="w-5 h-5 text-gray-400" />
)}
<button
onClick={onToggle}
className="p-1.5 hover:bg-gray-200 rounded-lg transition-colors cursor-pointer"
aria-label={isOpen ? "بستن" : "نمایش تنوع‌ها"}
>
{isOpen ? (
<ChevronUp className="w-5 h-5 text-gray-400" />
) : (
<ChevronDown className="w-5 h-5 text-gray-400" />
)}
</button>
</div>
</button>
</div>
</div>
{isOpen && variants && variants.length > 0 && (
@ -126,15 +171,16 @@ function ProductAccordionItem({
const size = variant.attributes?.find(
(attribute) => attribute.name === "SIZE"
);
const variantId = variant.id || "";
return (
<div
key={variant.id || `variant-${index}`}
className="flex items-center justify-between p-3 bg-WHITE rounded-lg border border-gray-100"
className="flex flex-col gap-3 p-3 bg-WHITE rounded-lg border border-gray-100 sm:flex-row sm:items-center sm:justify-between"
>
<div className="flex items-center justify-between gap-3">
<div className="flex items-center gap-3">
{color && (
<div
className="w-10 h-10 rounded-sm shadow-sm"
className="w-10 h-10 rounded-sm shadow-sm shrink-0"
style={{
backgroundColor: color.info.detail,
}}
@ -142,7 +188,7 @@ function ProductAccordionItem({
)}
{size && (
<div
className="w-10 h-10 flex items-center justify-center rounded-sm shadow-sm"
className="w-10 h-10 flex items-center justify-center rounded-sm shadow-sm shrink-0"
style={{
backgroundColor: "black",
color: "white",
@ -153,30 +199,58 @@ function ProductAccordionItem({
</div>
)}
</div>
<div className="flex-1 flex justify-end">
<ThrottledQuantityControl
key={`${variant.id}-${variant.stock}`}
initialQuantity={variant.stock || 0}
maxQuantity={999}
onQuantityUpdate={(newQuantity) => {
const variantId = variant.id || "";
setUpdatingVariantId(variantId);
updateVariantStock.mutate(
{
variantId,
stock: newQuantity,
},
{
onSettled: () => {
setUpdatingVariantId(null);
<div className="flex items-center justify-between gap-4 sm:justify-end">
{/* Per-variant price (overrides base price for this variant) */}
<div className="flex flex-col items-start gap-0.5">
<span className="text-[11px] text-GRAY">قیمت</span>
<InlinePriceEditor
value={variant.price}
onSave={(v) => {
setPricingVariantId(variantId);
updateVariantPrice.mutate(
{ variantId, price: v },
{ onSettled: () => setPricingVariantId(null) }
);
}}
isPending={pricingVariantId === variantId}
allowEmpty
placeholder={
faBase ? `پیش‌فرض: ${faBase}` : "پیش‌فرض"
}
size="sm"
align="start"
ariaLabel="ویرایش قیمت این تنوع"
className="-mr-2"
/>
</div>
{/* Stock */}
<div className="flex flex-col items-end gap-0.5">
<span className="text-[11px] text-GRAY">موجودی</span>
<ThrottledQuantityControl
key={`${variant.id}-${variant.stock}`}
initialQuantity={variant.stock || 0}
maxQuantity={999}
onQuantityUpdate={(newQuantity) => {
setUpdatingVariantId(variantId);
updateVariantStock.mutate(
{
variantId,
stock: newQuantity,
},
}
);
}}
isPending={updatingVariantId === variant.id}
allowManualEdit={true}
throttleDelay={1000}
/>
{
onSettled: () => {
setUpdatingVariantId(null);
},
}
);
}}
isPending={updatingVariantId === variant.id}
allowManualEdit={true}
throttleDelay={1000}
/>
</div>
</div>
</div>
);
@ -266,7 +340,7 @@ function StoreProductsContent() {
{/* Desktop table-style header */}
<div className="hidden lg:flex items-center px-4 py-3 text-GRAY text-[13px] font-bold border border-WHITE3 rounded-t-2xl bg-WHITE2">
<span className="flex-1">محصول</span>
<span className="w-32 text-center">قیمت</span>
<span className="w-48 text-center">قیمت</span>
<span className="w-24 text-center">تنوع</span>
<span className="w-28 text-center">عملیات</span>
</div>

View File

@ -31,6 +31,10 @@ import type {
SellerProductListItem,
VariantStockUpdateRequest,
VariantStockUpdateResponse,
ProductBasePriceUpdateRequest,
ProductBasePriceUpdateResponse,
VariantPriceUpdateRequest,
VariantPriceUpdateResponse,
} from '../models';
export interface ApiProductV1AttributeCreateRequest {
@ -105,6 +109,14 @@ export interface ApiProductV1ProductsVariantsStockPartialUpdateRequest {
data: VariantStockUpdateRequest;
}
export interface ApiProductV1ProductsBasePricePartialUpdateRequest {
data: ProductBasePriceUpdateRequest;
}
export interface ApiProductV1ProductsVariantsPricePartialUpdateRequest {
data: VariantPriceUpdateRequest;
}
export interface ApiProductV1SellerProductsListRequest {
search?: string;
ordering?: string;
@ -807,6 +819,82 @@ export class ProductManagementApi extends runtime.BaseAPI {
return await response.value();
}
/**
* Update **only** the base price of a product owned by (or managed for) the authenticated seller. Body includes `product_id` and the new `base_price`.
* Update product base price (seller only)
*/
async apiProductV1ProductsBasePricePartialUpdateRaw(requestParameters: ApiProductV1ProductsBasePricePartialUpdateRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise<runtime.ApiResponse<ProductBasePriceUpdateResponse>> {
if (requestParameters.data === null || requestParameters.data === undefined) {
throw new runtime.RequiredError('data','Required parameter requestParameters.data was null or undefined when calling apiProductV1ProductsBasePricePartialUpdate.');
}
const queryParameters: any = {};
const headerParameters: runtime.HTTPHeaders = {};
headerParameters['Content-Type'] = 'application/json';
if (this.configuration && (this.configuration.username !== undefined || this.configuration.password !== undefined)) {
headerParameters["Authorization"] = "Basic " + btoa(this.configuration.username + ":" + this.configuration.password);
}
const response = await this.request({
path: `/api/product/v1/products/base-price/`,
method: 'PATCH',
headers: headerParameters,
query: queryParameters,
body: requestParameters.data,
}, initOverrides);
return new runtime.JSONApiResponse(response);
}
/**
* Update **only** the base price of a product owned by (or managed for) the authenticated seller. Body includes `product_id` and the new `base_price`.
* Update product base price (seller only)
*/
async apiProductV1ProductsBasePricePartialUpdate(requestParameters: ApiProductV1ProductsBasePricePartialUpdateRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise<ProductBasePriceUpdateResponse> {
const response = await this.apiProductV1ProductsBasePricePartialUpdateRaw(requestParameters, initOverrides);
return await response.value();
}
/**
* Update **only** the per-variant price override of a product variant owned by (or managed for) the authenticated seller. Body includes `variant_id` and the new `price` (or `null` to inherit the product base price).
* Update variant price (seller only)
*/
async apiProductV1ProductsVariantsPricePartialUpdateRaw(requestParameters: ApiProductV1ProductsVariantsPricePartialUpdateRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise<runtime.ApiResponse<VariantPriceUpdateResponse>> {
if (requestParameters.data === null || requestParameters.data === undefined) {
throw new runtime.RequiredError('data','Required parameter requestParameters.data was null or undefined when calling apiProductV1ProductsVariantsPricePartialUpdate.');
}
const queryParameters: any = {};
const headerParameters: runtime.HTTPHeaders = {};
headerParameters['Content-Type'] = 'application/json';
if (this.configuration && (this.configuration.username !== undefined || this.configuration.password !== undefined)) {
headerParameters["Authorization"] = "Basic " + btoa(this.configuration.username + ":" + this.configuration.password);
}
const response = await this.request({
path: `/api/product/v1/products/variants/price/`,
method: 'PATCH',
headers: headerParameters,
query: queryParameters,
body: requestParameters.data,
}, initOverrides);
return new runtime.JSONApiResponse(response);
}
/**
* Update **only** the per-variant price override of a product variant owned by (or managed for) the authenticated seller. Body includes `variant_id` and the new `price` (or `null` to inherit the product base price).
* Update variant price (seller only)
*/
async apiProductV1ProductsVariantsPricePartialUpdate(requestParameters: ApiProductV1ProductsVariantsPricePartialUpdateRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise<VariantPriceUpdateResponse> {
const response = await this.apiProductV1ProductsVariantsPricePartialUpdateRaw(requestParameters, initOverrides);
return await response.value();
}
/**
* Return the authenticated seller\'s products with full attributes & variants. Ordering is strictly stable: - Products: -id (newest first) - Variants: id (ascending) - VariantAttributeMapping: id (ascending)
* List seller-owned products (stable by id)

View File

@ -6997,14 +6997,120 @@ export interface VariantStockUpdateResponse {
*/
readonly stock?: number;
/**
*
*
* @type {string}
* @memberof VariantStockUpdateResponse
*/
readonly updatedAt?: string;
}
/**
*
*
* @export
* @interface ProductBasePriceUpdateRequest
*/
export interface ProductBasePriceUpdateRequest {
/**
*
* @type {string}
* @memberof ProductBasePriceUpdateRequest
*/
productId: string;
/**
* New base price (>= 0)
* @type {string}
* @memberof ProductBasePriceUpdateRequest
*/
basePrice: string;
}
/**
*
* @export
* @interface ProductBasePriceUpdateResponse
*/
export interface ProductBasePriceUpdateResponse {
/**
*
* @type {string}
* @memberof ProductBasePriceUpdateResponse
*/
readonly id?: string;
/**
*
* @type {string}
* @memberof ProductBasePriceUpdateResponse
*/
readonly basePrice?: string;
/**
*
* @type {string}
* @memberof ProductBasePriceUpdateResponse
*/
readonly updatedAt?: string;
}
/**
*
* @export
* @interface VariantPriceUpdateRequest
*/
export interface VariantPriceUpdateRequest {
/**
*
* @type {string}
* @memberof VariantPriceUpdateRequest
*/
variantId: string;
/**
* Per-variant price override (>= 0), or null to inherit base price
* @type {string}
* @memberof VariantPriceUpdateRequest
*/
price: string | null;
}
/**
*
* @export
* @interface VariantPriceUpdateResponse
*/
export interface VariantPriceUpdateResponse {
/**
*
* @type {string}
* @memberof VariantPriceUpdateResponse
*/
readonly id?: string;
/**
*
* @type {string}
* @memberof VariantPriceUpdateResponse
*/
readonly product?: string;
/**
*
* @type {string}
* @memberof VariantPriceUpdateResponse
*/
readonly variantName?: string;
/**
*
* @type {string}
* @memberof VariantPriceUpdateResponse
*/
readonly price?: string | null;
/**
*
* @type {string}
* @memberof VariantPriceUpdateResponse
*/
readonly discountPrice?: string | null;
/**
*
* @type {string}
* @memberof VariantPriceUpdateResponse
*/
readonly updatedAt?: string;
}
/**
*
* @export
* @interface VectorSearchRequest
*/