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") businessId BigInt @map("business_id")
productId BigInt @map("product_id") productId BigInt @map("product_id")
isActive Boolean @default(true) @map("is_active") isActive Boolean @default(true) @map("is_active")
isUsed Boolean @default(false) @map("is_used")
sortOrder Int @default(0) @map("sort_order") sortOrder Int @default(0) @map("sort_order")
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(6) createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(6)
updatedAt DateTime @default(now()) @updatedAt @map("updated_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") @@unique([businessId, productId], map: "store_items_business_product_unique")
@@index([businessId], map: "idx_store_items_business_id") @@index([businessId], map: "idx_store_items_business_id")
@@index([productId], map: "idx_store_items_product_id") @@index([productId], map: "idx_store_items_product_id")
@@index([businessId, isUsed], map: "idx_store_items_business_is_used")
@@map("store_items") @@map("store_items")
} }
+20
View File
@@ -57,6 +57,11 @@ export class BatchCreateStoreItemsDto {
@MinLength(1) @MinLength(1)
productId!: string; productId!: string;
@IsOptional()
@Transform(({ value }) => value === 'true' || value === true)
@IsBoolean()
isUsed?: boolean;
@IsArray() @IsArray()
@ValidateNested({ each: true }) @ValidateNested({ each: true })
@Type(() => CreateStoreItemVariantDto) @Type(() => CreateStoreItemVariantDto)
@@ -79,6 +84,11 @@ export class ListStoreItemsDto {
@IsOptional() @IsOptional()
@IsString() @IsString()
name?: string; name?: string;
@IsOptional()
@Transform(({ value }) => value === 'true' || value === true)
@IsBoolean()
isUsed?: boolean;
} }
export class ListPublicStoreItemsDto { export class ListPublicStoreItemsDto {
@@ -120,6 +130,11 @@ export class ListPublicStoreItemsDto {
@IsBoolean() @IsBoolean()
isFestival?: boolean; isFestival?: boolean;
@IsOptional()
@Transform(({ value }) => value === 'true' || value === true)
@IsBoolean()
isUsed?: boolean;
@IsOptional() @IsOptional()
@Type(() => Number) @Type(() => Number)
@IsNumber() @IsNumber()
@@ -209,6 +224,11 @@ export class SyncProductStoreItemVariantsDto {
@MinLength(1) @MinLength(1)
productId!: string; productId!: string;
@IsOptional()
@Transform(({ value }) => value === 'true' || value === true)
@IsBoolean()
isUsed?: boolean;
@IsArray() @IsArray()
@ValidateNested({ each: true }) @ValidateNested({ each: true })
@Type(() => SyncStoreItemVariantRowDto) @Type(() => SyncStoreItemVariantRowDto)
+53 -15
View File
@@ -71,23 +71,44 @@ export class StoreItemsService {
const name = query.name?.trim(); const name = query.name?.trim();
const where: Prisma.StoreItemVariantWhereInput = { const where: Prisma.StoreItemVariantWhereInput = {
businessId, businessId,
...(name ...(query.isUsed
? { ? {
storeItem: { storeItem: {
product: { isUsed: true,
OR: [ ...(name
{ title: { contains: name, mode: 'insensitive' } }, ? {
{ product: {
content: { OR: [
path: ['nameFa'], { title: { contains: name, mode: 'insensitive' } },
string_contains: name, {
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([ const [items, total] = await Promise.all([
@@ -467,7 +488,9 @@ export class StoreItemsService {
} }
const created = await this.prisma.$transaction(async (tx) => { 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({ const baseSortOrder = await tx.storeItemVariant.count({
where: { storeItemId: storeItem.id }, where: { storeItemId: storeItem.id },
}); });
@@ -719,7 +742,9 @@ export class StoreItemsService {
} }
await this.prisma.$transaction(async (tx) => { 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({ const baseSortOrder = await tx.storeItemVariant.count({
where: { storeItemId: storeItem.id }, where: { storeItemId: storeItem.id },
}); });
@@ -947,17 +972,28 @@ export class StoreItemsService {
businessId: bigint, businessId: bigint,
productId: bigint, productId: bigint,
tx: Prisma.TransactionClient = this.prisma, tx: Prisma.TransactionClient = this.prisma,
options?: { isUsed?: boolean },
) { ) {
const existing = await tx.storeItem.findUnique({ const existing = await tx.storeItem.findUnique({
where: { businessId_productId: { businessId, productId } }, where: { businessId_productId: { businessId, productId } },
}); });
if (existing) { 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 existing;
} }
return tx.storeItem.create({ return tx.storeItem.create({
data: { businessId, productId }, data: {
businessId,
productId,
isUsed: options?.isUsed ?? false,
},
}); });
} }
@@ -1126,6 +1162,7 @@ export class StoreItemsService {
stockQuantity: variant.stockQuantity, stockQuantity: variant.stockQuantity,
rewardPoints: variant.rewardPoints, rewardPoints: variant.rewardPoints,
isFestival: variant.isFestival, isFestival: variant.isFestival,
isUsed: variant.storeItem.isUsed,
sortOrder: variant.sortOrder, sortOrder: variant.sortOrder,
createdAt: variant.createdAt, createdAt: variant.createdAt,
updatedAt: variant.updatedAt, updatedAt: variant.updatedAt,
@@ -1257,6 +1294,7 @@ export class StoreItemsService {
isActive: true, isActive: true,
storeItem: { storeItem: {
isActive: true, isActive: true,
...(query.isUsed ? { isUsed: true } : {}),
product: { product: {
status: ContentStatus.published, status: ContentStatus.published,
...(query.brandId ? { brandId: BigInt(query.brandId) } : {}), ...(query.brandId ? { brandId: BigInt(query.brandId) } : {}),
+1
View File
@@ -473,6 +473,7 @@ export class StoreSpecialsService {
productNameFa: (content.nameFa as string | null | undefined) ?? '', productNameFa: (content.nameFa as string | null | undefined) ?? '',
productImage, productImage,
isActive: storeItem.isActive, isActive: storeItem.isActive,
isUsed: storeItem.isUsed,
sortOrder: storeItem.sortOrder, sortOrder: storeItem.sortOrder,
variants: storeItem.variants.map((variant) => { variants: storeItem.variants.map((variant) => {
const selections = variant.selections.map((selection) => ({ const selections = variant.selections.map((selection) => ({