const crypto=require("crypto");const db=require("../../models"); const validateMedia=async(ids,userId,transaction)=>{if(!ids.length)return[];const rows=await db.Upload.findAll({where:{id:ids,status:"AVAILABLE"},transaction,lock:transaction?.LOCK?.UPDATE});const imageTypes=new Set(["image/jpeg","image/png","image/webp"]);if(rows.length!==new Set(ids).size||rows.some(x=>!imageTypes.has(x.file_type)||(x.owner_id!==userId&&x.owner_type!=="CATALOGUE")))throw Object.assign(new Error("Catalogue media must be a supported available image owned by the actor"),{status:403,code:"INVALID_MEDIA_OWNERSHIP"});return rows;}; const validatePublish=async(product,transaction)=>{const translation=await db.ProductTranslation.findOne({where:{product_id:product.id,locale:"en"},transaction});const variant=await db.ProductVariant.findOne({where:{product_id:product.id,status:"ACTIVE"},transaction});const media=await db.ProductMedia.findOne({where:{product_id:product.id,is_primary:true},transaction});if(!translation||!product.slug||!product.brand_id||!variant||!media)throw Object.assign(new Error("Publishing requires English content, brand, active variant, and primary media"),{status:409,code:"PUBLISH_REQUIREMENTS_NOT_MET"});}; const createProduct=(actor,body)=>db.sequelize.transaction(async t=>{await validateMedia(body.mediaUploadIds,actor.id,t);const product=await db.Product.create({id:crypto.randomUUID(),brand_id:body.brandId,default_category_id:body.defaultCategoryId||body.categoryIds[0],slug:body.slug,product_code:body.productCode,status:"DRAFT",visibility:"HIDDEN",product_type:body.productType,featured:body.featured,new_arrival_until:body.newArrivalUntil,created_by:actor.id,updated_by:actor.id},{transaction:t});await db.ProductTranslation.bulkCreate(body.translations.map(x=>({id:crypto.randomUUID(),product_id:product.id,locale:x.locale,name:x.name,short_description:x.shortDescription,description:x.description,care_instructions:x.careInstructions,materials:x.materials,origin:x.origin,meta_title:x.metaTitle,meta_description:x.metaDescription})),{transaction:t});await db.ProductCategory.bulkCreate([...new Set(body.categoryIds)].map(category_id=>({id:crypto.randomUUID(),product_id:product.id,category_id})),{transaction:t});await db.ProductVariant.bulkCreate(body.variants.map(x=>({id:crypto.randomUUID(),product_id:product.id,sku:x.sku,barcode:x.barcode,base_price:x.basePrice,compare_at_price:x.compareAtPrice,currency:x.currency,weight:x.weight,sort_order:x.sortOrder})),{transaction:t});if(body.mediaUploadIds.length){await db.ProductMedia.bulkCreate(body.mediaUploadIds.map((upload_id,i)=>({id:crypto.randomUUID(),product_id:product.id,upload_id,type:"IMAGE",sort_order:i,is_primary:i===0})),{transaction:t});await db.Upload.update({owner_type:"CATALOGUE",owner_id:product.id,use_for:"PRODUCT_MEDIA"},{where:{id:body.mediaUploadIds},transaction:t});}return product;}); const setProductState=(id,actor,updates)=>db.sequelize.transaction(async t=>{const product=await db.Product.findByPk(id,{transaction:t,lock:t.LOCK.UPDATE});if(!product)throw Object.assign(new Error("Product not found"),{status:404,code:"NOT_FOUND"});if(product.published_at&&updates.slug&&updates.slug!==product.slug)throw Object.assign(new Error("Published product slugs are immutable"),{status:409,code:"PUBLISHED_SLUG_IMMUTABLE"});if(updates.status==="ACTIVE"||updates.visibility==="PUBLIC")await validatePublish(product,t);const mapped={...(updates.brandId!==undefined&&{brand_id:updates.brandId}),...(updates.defaultCategoryId!==undefined&&{default_category_id:updates.defaultCategoryId}),...(updates.productType!==undefined&&{product_type:updates.productType}),...(updates.newArrivalUntil!==undefined&&{new_arrival_until:updates.newArrivalUntil}),...(updates.slug!==undefined&&{slug:updates.slug}),...(updates.featured!==undefined&&{featured:updates.featured}),...(updates.status!==undefined&&{status:updates.status}),...(updates.visibility!==undefined&&{visibility:updates.visibility})};await product.update({...mapped,updated_by:actor.id,...((updates.status==="ACTIVE"||updates.visibility==="PUBLIC")&&!product.published_at&&{published_at:new Date()})},{transaction:t});return product;}); const assertCategoryParent=async(id,parentId,transaction)=>{if(!parentId)return;if(id===parentId)throw Object.assign(new Error("Category cannot parent itself"),{status:400,code:"CATEGORY_SELF_PARENT"});let cursor=parentId;const seen=new Set([id]);for(let depth=0;cursor&&depth<100;depth++){if(seen.has(cursor))throw Object.assign(new Error("Category hierarchy cycle detected"),{status:409,code:"CATEGORY_CYCLE"});seen.add(cursor);const node=await db.Category.findByPk(cursor,{transaction,attributes:["id","parent_id"]});if(!node)throw Object.assign(new Error("Parent category not found"),{status:400,code:"INVALID_PARENT"});cursor=node.parent_id;}if(cursor)throw Object.assign(new Error("Category hierarchy exceeds safe depth"),{status:400,code:"CATEGORY_DEPTH"});}; module.exports={createProduct,setProductState,validatePublish,validateMedia,assertCategoryParent};