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:
co-authored by
Cursor
parent
c6b7629c77
commit
94e9a8ed1c
@@ -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);
|
||||||
@@ -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")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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) } : {}),
|
||||||
|
|||||||
@@ -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) => ({
|
||||||
|
|||||||
Reference in New Issue
Block a user