diff --git a/database/migrations/094_store_item_is_used.sql b/database/migrations/094_store_item_is_used.sql new file mode 100644 index 0000000..856c189 --- /dev/null +++ b/database/migrations/094_store_item_is_used.sql @@ -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); diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 294a131..362c292 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -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") } diff --git a/src/store/dto/store-items.dto.ts b/src/store/dto/store-items.dto.ts index 59eb0a2..a612172 100644 --- a/src/store/dto/store-items.dto.ts +++ b/src/store/dto/store-items.dto.ts @@ -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) diff --git a/src/store/store-items.service.ts b/src/store/store-items.service.ts index 19b9cb6..ed8e329 100644 --- a/src/store/store-items.service.ts +++ b/src/store/store-items.service.ts @@ -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) } : {}), diff --git a/src/store/store-specials.service.ts b/src/store/store-specials.service.ts index 9ff3c53..fd67e4e 100644 --- a/src/store/store-specials.service.ts +++ b/src/store/store-specials.service.ts @@ -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) => ({