merge: release AI render catalog scope
This commit is contained in:
commit
400bdaf1ef
@ -0,0 +1,122 @@
|
||||
jest.mock('@/lib/prisma', () => ({
|
||||
prisma: {
|
||||
user: {
|
||||
findMany: jest.fn(),
|
||||
},
|
||||
intakeNonIntegratedProduct: {
|
||||
findMany: jest.fn(),
|
||||
findFirst: jest.fn(),
|
||||
},
|
||||
},
|
||||
}))
|
||||
|
||||
import { prisma } from '@/lib/prisma'
|
||||
import { IntakeAiAssetRepository } from '../intake-ai-asset.repository'
|
||||
import {
|
||||
buildDevelopmentKindWhere,
|
||||
buildProductKindWhere,
|
||||
} from '../intake-product-list-filters'
|
||||
|
||||
const mockPrisma = prisma as unknown as {
|
||||
user: {
|
||||
findMany: jest.Mock
|
||||
}
|
||||
intakeNonIntegratedProduct: {
|
||||
findMany: jest.Mock
|
||||
findFirst: jest.Mock
|
||||
}
|
||||
}
|
||||
|
||||
describe('IntakeAiAssetRepository catalog scope', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
mockPrisma.user.findMany.mockResolvedValue([
|
||||
{ id: 'external-intake-1' },
|
||||
{ id: 'external-intake-2' },
|
||||
])
|
||||
mockPrisma.intakeNonIntegratedProduct.findMany.mockResolvedValue([])
|
||||
mockPrisma.intakeNonIntegratedProduct.findFirst.mockResolvedValue(null)
|
||||
})
|
||||
|
||||
it('lists all product-development SPUs while excluding stock, series, and external intake', async () => {
|
||||
const repository = new IntakeAiAssetRepository()
|
||||
|
||||
await repository.findCatalogProducts('')
|
||||
|
||||
expect(mockPrisma.user.findMany).toHaveBeenCalledWith({
|
||||
where: {
|
||||
businessRoles: {
|
||||
has: 'EXTERNAL_INTAKE',
|
||||
},
|
||||
},
|
||||
select: { id: true },
|
||||
})
|
||||
expect(mockPrisma.intakeNonIntegratedProduct.findMany).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
where: {
|
||||
deletedAt: null,
|
||||
AND: [
|
||||
buildDevelopmentKindWhere('product'),
|
||||
buildProductKindWhere('new'),
|
||||
{
|
||||
createdBy: {
|
||||
notIn: ['external-intake-1', 'external-intake-2'],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
)
|
||||
|
||||
const query = mockPrisma.intakeNonIntegratedProduct.findMany.mock.calls[0][0]
|
||||
expect(query).not.toHaveProperty('take')
|
||||
})
|
||||
|
||||
it('keeps SPU and barcode search inside the same catalog scope', async () => {
|
||||
const repository = new IntakeAiAssetRepository()
|
||||
|
||||
await repository.findCatalogProducts(' BAR-001 ')
|
||||
|
||||
const query = mockPrisma.intakeNonIntegratedProduct.findMany.mock.calls[0][0]
|
||||
const searchWhere = query.where.AND[3]
|
||||
|
||||
expect(searchWhere.OR).toEqual(
|
||||
expect.arrayContaining([
|
||||
{ spu: { contains: 'BAR-001', mode: 'insensitive' } },
|
||||
expect.objectContaining({
|
||||
skuItems: {
|
||||
some: {
|
||||
OR: expect.arrayContaining([
|
||||
{ barcode: { contains: 'BAR-001', mode: 'insensitive' } },
|
||||
]),
|
||||
},
|
||||
},
|
||||
}),
|
||||
]),
|
||||
)
|
||||
})
|
||||
|
||||
it('applies the same scope when a product workspace is opened directly', async () => {
|
||||
const repository = new IntakeAiAssetRepository()
|
||||
|
||||
await repository.findProductById('product-1')
|
||||
|
||||
expect(mockPrisma.intakeNonIntegratedProduct.findFirst).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
where: {
|
||||
id: 'product-1',
|
||||
deletedAt: null,
|
||||
AND: [
|
||||
buildDevelopmentKindWhere('product'),
|
||||
buildProductKindWhere('new'),
|
||||
{
|
||||
createdBy: {
|
||||
notIn: ['external-intake-1', 'external-intake-2'],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
)
|
||||
})
|
||||
})
|
||||
@ -1,5 +1,9 @@
|
||||
import { Prisma } from '@prisma/client'
|
||||
import { prisma } from '@/lib/prisma'
|
||||
import {
|
||||
buildDevelopmentKindWhere,
|
||||
buildProductKindWhere,
|
||||
} from './intake-product-list-filters'
|
||||
|
||||
const skuAiProfileInclude = {
|
||||
currentModelVersion: {
|
||||
@ -43,14 +47,27 @@ export interface CreateModelVersionInput {
|
||||
createdBy: string
|
||||
}
|
||||
|
||||
export class IntakeAiAssetRepository {
|
||||
findCatalogProducts(query: string, take = 200) {
|
||||
const normalizedQuery = query.trim()
|
||||
return prisma.intakeNonIntegratedProduct.findMany({
|
||||
where: {
|
||||
deletedAt: null,
|
||||
...(normalizedQuery
|
||||
? {
|
||||
function buildCatalogWhere(input: {
|
||||
query?: string
|
||||
productId?: string
|
||||
excludedCreatorIds: string[]
|
||||
}): Prisma.IntakeNonIntegratedProductWhereInput {
|
||||
const normalizedQuery = input.query?.trim() ?? ''
|
||||
const developmentWhere = buildDevelopmentKindWhere('product')
|
||||
const productKindWhere = buildProductKindWhere('new')
|
||||
|
||||
return {
|
||||
...(input.productId ? { id: input.productId } : {}),
|
||||
deletedAt: null,
|
||||
AND: [
|
||||
...(developmentWhere ? [developmentWhere] : []),
|
||||
...(productKindWhere ? [productKindWhere] : []),
|
||||
...(input.excludedCreatorIds.length > 0
|
||||
? [{ createdBy: { notIn: input.excludedCreatorIds } }]
|
||||
: []),
|
||||
...(normalizedQuery
|
||||
? [
|
||||
{
|
||||
OR: [
|
||||
{ spu: { contains: normalizedQuery, mode: 'insensitive' as const } },
|
||||
{ spuBase: { contains: normalizedQuery, mode: 'insensitive' as const } },
|
||||
@ -71,18 +88,40 @@ export class IntakeAiAssetRepository {
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
: {}),
|
||||
},
|
||||
]
|
||||
: []),
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
export class IntakeAiAssetRepository {
|
||||
private async findExcludedCatalogCreatorIds() {
|
||||
const users = await prisma.user.findMany({
|
||||
where: {
|
||||
businessRoles: {
|
||||
has: 'EXTERNAL_INTAKE',
|
||||
},
|
||||
},
|
||||
select: { id: true },
|
||||
})
|
||||
|
||||
return users.map((user) => user.id)
|
||||
}
|
||||
|
||||
async findCatalogProducts(query: string) {
|
||||
const excludedCreatorIds = await this.findExcludedCatalogCreatorIds()
|
||||
return prisma.intakeNonIntegratedProduct.findMany({
|
||||
where: buildCatalogWhere({ query, excludedCreatorIds }),
|
||||
include: catalogProductInclude,
|
||||
orderBy: [{ updatedAt: 'desc' }, { createdAt: 'desc' }],
|
||||
take,
|
||||
})
|
||||
}
|
||||
|
||||
findProductById(productId: string) {
|
||||
async findProductById(productId: string) {
|
||||
const excludedCreatorIds = await this.findExcludedCatalogCreatorIds()
|
||||
return prisma.intakeNonIntegratedProduct.findFirst({
|
||||
where: { id: productId, deletedAt: null },
|
||||
where: buildCatalogWhere({ productId, excludedCreatorIds }),
|
||||
include: catalogProductInclude,
|
||||
})
|
||||
}
|
||||
|
||||
@ -0,0 +1,169 @@
|
||||
import { Prisma } from '@prisma/client'
|
||||
|
||||
type ProductDevelopmentKind = 'product' | 'series' | undefined
|
||||
type ProductKind = 'all' | 'new' | 'stock' | undefined
|
||||
|
||||
const JST_SKU_SOURCE_PATHS = [
|
||||
['source'],
|
||||
['sourceTable'],
|
||||
['importSource'],
|
||||
['externalSource'],
|
||||
['metadata', 'source'],
|
||||
['metadata', 'sourceTable'],
|
||||
['metadata', 'importSource'],
|
||||
['metadata', 'externalSource'],
|
||||
] as const
|
||||
|
||||
function jsonTextEquals(
|
||||
path: string[],
|
||||
value: string,
|
||||
): Prisma.IntakeNonIntegratedProductWhereInput {
|
||||
return {
|
||||
payload: {
|
||||
path,
|
||||
equals: value,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
function jsonTextMissingOrNotEquals(
|
||||
path: string[],
|
||||
value: string,
|
||||
): Prisma.IntakeNonIntegratedProductWhereInput {
|
||||
return {
|
||||
OR: [
|
||||
{
|
||||
NOT: jsonTextEquals(path, value),
|
||||
},
|
||||
{
|
||||
payload: {
|
||||
path,
|
||||
equals: Prisma.AnyNull,
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
function profileTextEquals(
|
||||
field: string,
|
||||
value: string,
|
||||
): Prisma.IntakeNonIntegratedProductWhereInput {
|
||||
return {
|
||||
profile: {
|
||||
is: {
|
||||
[field]: value,
|
||||
},
|
||||
},
|
||||
} as Prisma.IntakeNonIntegratedProductWhereInput
|
||||
}
|
||||
|
||||
function profileTextNonEmpty(field: string): Prisma.IntakeNonIntegratedProductWhereInput {
|
||||
return {
|
||||
profile: {
|
||||
is: {
|
||||
[field]: {
|
||||
not: '',
|
||||
},
|
||||
},
|
||||
},
|
||||
} as Prisma.IntakeNonIntegratedProductWhereInput
|
||||
}
|
||||
|
||||
function payloadJsonWhenProfileMissing(
|
||||
condition: Prisma.IntakeNonIntegratedProductWhereInput,
|
||||
): Prisma.IntakeNonIntegratedProductWhereInput {
|
||||
return {
|
||||
AND: [
|
||||
{
|
||||
profile: null,
|
||||
},
|
||||
condition,
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
function profileTextNotEqualsWhenPresent(
|
||||
field: string,
|
||||
value: string,
|
||||
): Prisma.IntakeNonIntegratedProductWhereInput {
|
||||
return {
|
||||
AND: [
|
||||
{
|
||||
NOT: {
|
||||
profile: null,
|
||||
},
|
||||
},
|
||||
{
|
||||
OR: [
|
||||
{
|
||||
NOT: profileTextEquals(field, value),
|
||||
},
|
||||
{
|
||||
profile: {
|
||||
is: {
|
||||
[field]: '',
|
||||
},
|
||||
},
|
||||
} as Prisma.IntakeNonIntegratedProductWhereInput,
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
export function buildStockProductWhere(): Prisma.IntakeNonIntegratedProductWhereInput {
|
||||
return {
|
||||
OR: [
|
||||
{ id: { startsWith: 'jst-stock-' } },
|
||||
profileTextNonEmpty('sourceSpu'),
|
||||
...JST_SKU_SOURCE_PATHS.map((path) =>
|
||||
payloadJsonWhenProfileMissing(jsonTextEquals([...path], 'jst_sku')),
|
||||
),
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
export function buildDevelopmentKindWhere(
|
||||
productDevelopmentKind: ProductDevelopmentKind,
|
||||
): Prisma.IntakeNonIntegratedProductWhereInput | null {
|
||||
if (productDevelopmentKind === 'series') {
|
||||
return {
|
||||
OR: [
|
||||
profileTextEquals('productDevelopmentKind', 'series'),
|
||||
payloadJsonWhenProfileMissing(jsonTextEquals(['productDevelopmentKind'], 'series')),
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
if (productDevelopmentKind === 'product') {
|
||||
return {
|
||||
OR: [
|
||||
profileTextNotEqualsWhenPresent('productDevelopmentKind', 'series'),
|
||||
payloadJsonWhenProfileMissing(
|
||||
jsonTextMissingOrNotEquals(['productDevelopmentKind'], 'series'),
|
||||
),
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
export function buildProductKindWhere(
|
||||
productKind: ProductKind,
|
||||
): Prisma.IntakeNonIntegratedProductWhereInput | null {
|
||||
const stockWhere = buildStockProductWhere()
|
||||
|
||||
if (productKind === 'stock') {
|
||||
return stockWhere
|
||||
}
|
||||
|
||||
if (productKind === 'new') {
|
||||
return {
|
||||
NOT: stockWhere,
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
@ -51,6 +51,11 @@ import {
|
||||
intakeNonIntegratedProductRepository,
|
||||
type IntakeNonIntegratedProductRepositoryClass,
|
||||
} from '../repositories/intake-non-integrated-product.repository'
|
||||
import {
|
||||
buildDevelopmentKindWhere,
|
||||
buildProductKindWhere,
|
||||
buildStockProductWhere,
|
||||
} from '../repositories/intake-product-list-filters'
|
||||
import { userRepository } from '../repositories/user.repository'
|
||||
import {
|
||||
assetSkuGroupRepository,
|
||||
@ -3066,25 +3071,6 @@ function jsonTextContains(
|
||||
}
|
||||
}
|
||||
|
||||
function jsonTextMissingOrNotEquals(
|
||||
path: string[],
|
||||
value: string,
|
||||
): Prisma.IntakeNonIntegratedProductWhereInput {
|
||||
return {
|
||||
OR: [
|
||||
{
|
||||
NOT: jsonTextEquals(path, value),
|
||||
},
|
||||
{
|
||||
payload: {
|
||||
path,
|
||||
equals: Prisma.AnyNull,
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
function profileTextEquals(
|
||||
field: string,
|
||||
value: string,
|
||||
@ -3139,35 +3125,6 @@ function payloadJsonWhenProfileMissing(
|
||||
}
|
||||
}
|
||||
|
||||
function profileTextNotEqualsWhenPresent(
|
||||
field: string,
|
||||
value: string,
|
||||
): Prisma.IntakeNonIntegratedProductWhereInput {
|
||||
return {
|
||||
AND: [
|
||||
{
|
||||
NOT: {
|
||||
profile: null,
|
||||
},
|
||||
},
|
||||
{
|
||||
OR: [
|
||||
{
|
||||
NOT: profileTextEquals(field, value),
|
||||
},
|
||||
{
|
||||
profile: {
|
||||
is: {
|
||||
[field]: '',
|
||||
},
|
||||
},
|
||||
} as Prisma.IntakeNonIntegratedProductWhereInput,
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
function payloadTextContainsWhenProfileMissing(path: string[], keyword: string) {
|
||||
return payloadJsonWhenProfileMissing(jsonTextContains(path, keyword))
|
||||
}
|
||||
@ -3184,71 +3141,6 @@ function buildTextContainsAny(
|
||||
}
|
||||
}
|
||||
|
||||
const JST_SKU_SOURCE_PATHS = [
|
||||
['source'],
|
||||
['sourceTable'],
|
||||
['importSource'],
|
||||
['externalSource'],
|
||||
['metadata', 'source'],
|
||||
['metadata', 'sourceTable'],
|
||||
['metadata', 'importSource'],
|
||||
['metadata', 'externalSource'],
|
||||
] as const
|
||||
|
||||
function buildStockProductWhere(): Prisma.IntakeNonIntegratedProductWhereInput {
|
||||
return {
|
||||
OR: [
|
||||
{ id: { startsWith: 'jst-stock-' } },
|
||||
profileTextNonEmpty('sourceSpu'),
|
||||
...JST_SKU_SOURCE_PATHS.map((path) =>
|
||||
payloadJsonWhenProfileMissing(jsonTextEquals([...path], 'jst_sku')),
|
||||
),
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
function buildDevelopmentKindWhere(
|
||||
productDevelopmentKind: IntakeNonIntegratedProductListParams['productDevelopmentKind'],
|
||||
): Prisma.IntakeNonIntegratedProductWhereInput | null {
|
||||
if (productDevelopmentKind === 'series') {
|
||||
return {
|
||||
OR: [
|
||||
profileTextEquals('productDevelopmentKind', 'series'),
|
||||
payloadJsonWhenProfileMissing(jsonTextEquals(['productDevelopmentKind'], 'series')),
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
if (productDevelopmentKind === 'product') {
|
||||
return {
|
||||
OR: [
|
||||
profileTextNotEqualsWhenPresent('productDevelopmentKind', 'series'),
|
||||
payloadJsonWhenProfileMissing(jsonTextMissingOrNotEquals(['productDevelopmentKind'], 'series')),
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
function buildProductKindWhere(
|
||||
productKind: IntakeNonIntegratedProductListParams['productKind'],
|
||||
): Prisma.IntakeNonIntegratedProductWhereInput | null {
|
||||
const stockWhere = buildStockProductWhere()
|
||||
|
||||
if (productKind === 'stock') {
|
||||
return stockWhere
|
||||
}
|
||||
|
||||
if (productKind === 'new') {
|
||||
return {
|
||||
NOT: stockWhere,
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
function buildSpuSearchWhere(spu: string): Prisma.IntakeNonIntegratedProductWhereInput | null {
|
||||
const keyword = trimListFilter(spu)
|
||||
if (!keyword) return null
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user