Add isUsed on store items with API list filters.

Persists used/second-hand status per listing and supports isUsed=true on business and public store-item queries.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Alireza Hassani
2026-09-19 12:17:28 +03:30
co-authored by Cursor
parent c6b7629c77
commit 94e9a8ed1c
5 changed files with 82 additions and 15 deletions
@@ -0,0 +1,6 @@
-- Used / second-hand flag on store listings (product-level).
ALTER TABLE store_items
ADD COLUMN IF NOT EXISTS is_used BOOLEAN NOT NULL DEFAULT FALSE;
CREATE INDEX IF NOT EXISTS idx_store_items_business_is_used
ON store_items (business_id, is_used);
+2
View File
@@ -1141,6 +1141,7 @@ model StoreItem {
businessId BigInt @map("business_id")
productId BigInt @map("product_id")
isActive Boolean @default(true) @map("is_active")
isUsed Boolean @default(false) @map("is_used")
sortOrder Int @default(0) @map("sort_order")
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(6)
updatedAt DateTime @default(now()) @updatedAt @map("updated_at") @db.Timestamptz(6)
@@ -1152,6 +1153,7 @@ model StoreItem {
@@unique([businessId, productId], map: "store_items_business_product_unique")
@@index([businessId], map: "idx_store_items_business_id")
@@index([productId], map: "idx_store_items_product_id")
@@index([businessId, isUsed], map: "idx_store_items_business_is_used")
@@map("store_items")
}
+20
View File
@@ -57,6 +57,11 @@ export class BatchCreateStoreItemsDto {
@MinLength(1)
productId!: string;
@IsOptional()
@Transform(({ value }) => value === 'true' || value === true)
@IsBoolean()
isUsed?: boolean;
@IsArray()
@ValidateNested({ each: true })
@Type(() => CreateStoreItemVariantDto)
@@ -79,6 +84,11 @@ export class ListStoreItemsDto {
@IsOptional()
@IsString()
name?: string;
@IsOptional()
@Transform(({ value }) => value === 'true' || value === true)
@IsBoolean()
isUsed?: boolean;
}
export class ListPublicStoreItemsDto {
@@ -120,6 +130,11 @@ export class ListPublicStoreItemsDto {
@IsBoolean()
isFestival?: boolean;
@IsOptional()
@Transform(({ value }) => value === 'true' || value === true)
@IsBoolean()
isUsed?: boolean;
@IsOptional()
@Type(() => Number)
@IsNumber()
@@ -209,6 +224,11 @@ export class SyncProductStoreItemVariantsDto {
@MinLength(1)
productId!: string;
@IsOptional()
@Transform(({ value }) => value === 'true' || value === true)
@IsBoolean()
isUsed?: boolean;
@IsArray()
@ValidateNested({ each: true })
@Type(() => SyncStoreItemVariantRowDto)
+53 -15
View File
@@ -71,23 +71,44 @@ export class StoreItemsService {
const name = query.name?.trim();
const where: Prisma.StoreItemVariantWhereInput = {
businessId,
...(name
...(query.isUsed
? {
storeItem: {
product: {
OR: [
{ title: { contains: name, mode: 'insensitive' } },
{
content: {
path: ['nameFa'],
string_contains: name,
isUsed: true,
...(name
? {
product: {
OR: [
{ title: { contains: name, mode: 'insensitive' } },
{
content: {
path: ['nameFa'],
string_contains: name,
},
},
],
},
},
],
},
}
: {}),
},
}
: {}),
: name
? {
storeItem: {
product: {
OR: [
{ title: { contains: name, mode: 'insensitive' } },
{
content: {
path: ['nameFa'],
string_contains: name,
},
},
],
},
},
}
: {}),
};
const [items, total] = await Promise.all([
@@ -467,7 +488,9 @@ export class StoreItemsService {
}
const created = await this.prisma.$transaction(async (tx) => {
const storeItem = await this.ensureStoreItem(businessId, productId, tx);
const storeItem = await this.ensureStoreItem(businessId, productId, tx, {
isUsed: dto.isUsed,
});
const baseSortOrder = await tx.storeItemVariant.count({
where: { storeItemId: storeItem.id },
});
@@ -719,7 +742,9 @@ export class StoreItemsService {
}
await this.prisma.$transaction(async (tx) => {
const storeItem = await this.ensureStoreItem(businessId, productId, tx);
const storeItem = await this.ensureStoreItem(businessId, productId, tx, {
isUsed: dto.isUsed,
});
const baseSortOrder = await tx.storeItemVariant.count({
where: { storeItemId: storeItem.id },
});
@@ -947,17 +972,28 @@ export class StoreItemsService {
businessId: bigint,
productId: bigint,
tx: Prisma.TransactionClient = this.prisma,
options?: { isUsed?: boolean },
) {
const existing = await tx.storeItem.findUnique({
where: { businessId_productId: { businessId, productId } },
});
if (existing) {
if (options?.isUsed !== undefined && existing.isUsed !== options.isUsed) {
return tx.storeItem.update({
where: { id: existing.id },
data: { isUsed: options.isUsed },
});
}
return existing;
}
return tx.storeItem.create({
data: { businessId, productId },
data: {
businessId,
productId,
isUsed: options?.isUsed ?? false,
},
});
}
@@ -1126,6 +1162,7 @@ export class StoreItemsService {
stockQuantity: variant.stockQuantity,
rewardPoints: variant.rewardPoints,
isFestival: variant.isFestival,
isUsed: variant.storeItem.isUsed,
sortOrder: variant.sortOrder,
createdAt: variant.createdAt,
updatedAt: variant.updatedAt,
@@ -1257,6 +1294,7 @@ export class StoreItemsService {
isActive: true,
storeItem: {
isActive: true,
...(query.isUsed ? { isUsed: true } : {}),
product: {
status: ContentStatus.published,
...(query.brandId ? { brandId: BigInt(query.brandId) } : {}),
+1
View File
@@ -473,6 +473,7 @@ export class StoreSpecialsService {
productNameFa: (content.nameFa as string | null | undefined) ?? '',
productImage,
isActive: storeItem.isActive,
isUsed: storeItem.isUsed,
sortOrder: storeItem.sortOrder,
variants: storeItem.variants.map((variant) => {
const selections = variant.selections.map((selection) => ({