Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 20 additions & 4 deletions src/docs/schemas/commerce/product.schema.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ export const productSchemas = {
properties: {
name: { type: "string", maxLength: 100, example: "Auriculares Bluetooth" },
price: { type: "number", example: 150000 },
offerPrice: { type: "number", nullable: true, example: 120000 },
isOffer: { type: "boolean", example: true },
categoryId: { type: "integer", example: 3 },
description: { type: "string", nullable: true, example: "Auriculares de alta calidad" },
quantity: { type: "integer", nullable: true, example: 50 },
Expand All @@ -23,6 +25,8 @@ export const productSchemas = {
properties: {
name: { type: "string", maxLength: 100, example: "Auriculares Bluetooth Pro" },
price: { type: "number", example: 200000 },
offerPrice: { type: "number", nullable: true, example: 150000 },
isOffer: { type: "boolean", example: true },
categoryId: { type: "integer", example: 3 },
description: { type: "string", nullable: true },
quantity: { type: "integer", nullable: true },
Expand All @@ -38,7 +42,10 @@ export const productSchemas = {
id: { type: "integer", example: 1 },
name: { type: "string", example: "Auriculares Bluetooth" },
description: { type: "string", nullable: true },
price: { type: "number", example: 150000 },
price: { type: "number", example: 120000 },
originalPrice: { type: "number", example: 150000 },
offerPrice: { type: "number", nullable: true, example: 120000 },
isOffer: { type: "boolean", example: true },
quantity: { type: "integer", nullable: true, example: 50 },
categoryId: { type: "integer", example: 3 },
category: {
Expand Down Expand Up @@ -83,7 +90,10 @@ export const productSchemas = {
id: { type: "integer", example: 1 },
name: { type: "string", example: "Auriculares Bluetooth" },
description: { type: "string", nullable: true },
price: { type: "number", example: 150000 },
price: { type: "number", example: 120000 },
original_price: { type: "number", example: 150000 },
offer_price: { type: "number", nullable: true, example: 120000 },
is_offer: { type: "boolean", example: true },
store: {
type: "object",
nullable: true,
Expand Down Expand Up @@ -124,7 +134,10 @@ export const productSchemas = {
productId: { type: "integer", example: 1 },
name: { type: "string", example: "Auriculares Bluetooth" },
description: { type: "string", nullable: true },
price: { type: "number", example: 150000 },
price: { type: "number", example: 120000 },
originalPrice: { type: "number", example: 150000 },
offerPrice: { type: "number", nullable: true, example: 120000 },
isOffer: { type: "boolean", example: true },
store: {
type: "object",
nullable: true,
Expand All @@ -144,6 +157,9 @@ export const productSchemas = {
name: { type: "string", example: "Auriculares Bluetooth" },
description: { type: "string", nullable: true },
price: { type: "number", example: 120000 },
originalPrice: { type: "number", example: 150000 },
offerPrice: { type: "number", nullable: true, example: 120000 },
isOffer: { type: "boolean", example: true },
store: {
type: "object",
nullable: true,
Expand All @@ -167,4 +183,4 @@ export const productSchemas = {
}
}
}
};
};
25 changes: 25 additions & 0 deletions src/lib/product-pricing.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
const toNullableNumber = (value) => {
if (value === undefined || value === null) {
return null;
}

return Number(value);
};

export const getOriginalProductPrice = (product) => Number(product.price);

export const getOfferProductPrice = (product) => toNullableNumber(product.offer_price);

export const getEffectiveProductPrice = (product) => {
const originalPrice = getOriginalProductPrice(product);
const offerPrice = getOfferProductPrice(product);

return product?.is_offer && offerPrice !== null ? offerPrice : originalPrice;
};

export const getProductPricing = (product) => ({
price: getEffectiveProductPrice(product),
originalPrice: getOriginalProductPrice(product),
offerPrice: getOfferProductPrice(product),
isOffer: Boolean(product?.is_offer)
});
97 changes: 87 additions & 10 deletions src/modules/commerce/commerces/store.service.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import fs from "node:fs/promises";
import path from "node:path";
import { prisma } from "../../../lib/prisma.js";
import { getProductPricing } from "../../../lib/product-pricing.js";
import { validateStoreCategoryService } from "../store-categories/store-category.service.js";

const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
Expand Down Expand Up @@ -43,8 +44,10 @@ const STORE_RESPONSE_SELECT = {
id_product: true,
name: true,
price: true,
offer_price: true,
quantity: true,
visible: true,
is_offer: true,
product_category: {
select: {
id_product_category: true,
Expand Down Expand Up @@ -149,6 +152,52 @@ const validateEmailField = (value) => {
return normalizedEmail;
};

const parseBooleanField = (value, fieldName) => {
if (typeof value === "boolean") {
return value;
}

const normalizedValue = String(value).trim().toLowerCase();

if (normalizedValue === "true" || normalizedValue === "1") {
return true;
}

if (normalizedValue === "false" || normalizedValue === "0") {
return false;
}

throw {
status: 400,
message: `${fieldName} debe ser booleano`
};
};

const mapStoreProductPricing = (product) => {
const pricing = getProductPricing(product);

return {
...product,
price: pricing.price,
original_price: pricing.originalPrice,
offer_price: pricing.offerPrice,
is_offer: pricing.isOffer
};
};

const mapStoreWithPricedProducts = (store) => {
if (!store) {
return store;
}

return {
...store,
products: Array.isArray(store.products)
? store.products.map(mapStoreProductPricing)
: []
};
};

// valida que el usuario autenticado sea el propietario del comercio solicitado
export const getAuthorizedStoreOwnerService = async (
authenticatedUserId,
Expand Down Expand Up @@ -571,7 +620,7 @@ export const updateStoreService = async (

await deletePreviousStoreLogoFromStorage(store.logo, updatedStore.logo);

return updatedStore;
return mapStoreWithPricedProducts(updatedStore);
} catch (error) {
if (error.code === "P2002") {
throw {
Expand Down Expand Up @@ -622,8 +671,10 @@ export const getStoreByIdService = async (id) => {
id_product: true,
name: true,
price: true,
offer_price: true,
quantity: true,
visible: true,
is_offer: true,
product_category: {
select: { id_product_category: true, name: true }
}
Expand All @@ -641,7 +692,7 @@ export const getStoreByIdService = async (id) => {
throw { status: 404, message: "Comercio no encontrado" };
}

return store;
return mapStoreWithPricedProducts(store);

} catch (error) {
if (error.status) {
Expand Down Expand Up @@ -688,8 +739,10 @@ export const getAllProductsByStoreService = async (id) => {
name: true,
description: true,
price: true,
offer_price: true,
quantity: true,
visible: true,
is_offer: true,
created_at: true,
product_category: {
select: { id_product_category: true, name: true },
Expand All @@ -705,7 +758,7 @@ export const getAllProductsByStoreService = async (id) => {
};
}

return products;
return products.map(mapStoreProductPricing);

} catch (error) {
if (error.status) {
Expand Down Expand Up @@ -743,10 +796,13 @@ export const filterStoreProductsService = async (id, filters, pagination) => {
name,
category,
visible,
available,
minPrice,
maxPrice,
price_min,
price_max,
isOffer,
is_offer,
sortBy,
sortOrder
} = filters;
Expand All @@ -764,8 +820,17 @@ export const filterStoreProductsService = async (id, filters, pagination) => {
whereConditions.fk_product_category = Number(category);
}

if (visible !== undefined && visible !== null) {
whereConditions.visible = visible;
const resolvedVisible = visible ?? available;
if (resolvedVisible !== undefined && resolvedVisible !== null) {
whereConditions.visible = resolvedVisible;
}

const resolvedIsOffer = isOffer ?? is_offer;
if (resolvedIsOffer !== undefined && resolvedIsOffer !== null) {
whereConditions.is_offer =
typeof resolvedIsOffer === "boolean"
? resolvedIsOffer
: parseBooleanField(resolvedIsOffer, "isOffer");
}

const resolvedMinPrice = minPrice ?? price_min;
Expand All @@ -782,31 +847,43 @@ export const filterStoreProductsService = async (id, filters, pagination) => {
};
}

const products = await prisma.products.findMany({
const [totalProducts, products] = await Promise.all([
prisma.products.count({
where: whereConditions
}),
prisma.products.findMany({
where: whereConditions,
skip: pagination?.skip ?? 0,
take: pagination?.limit ?? 20,
select: {
id_product: true,
name: true,
description: true,
price: true,
offer_price: true,
quantity: true,
visible: true,
is_offer: true,
created_at: true,
product_category: {
select: { id_product_category: true, name: true },
},
},
orderBy: { [sortBy || "created_at"]: sortOrder === "asc" ? "asc" : "desc" }
});
})
]);

if (!products || products.length === 0) {
if (totalProducts === 0) {
throw {
status: 404,
message: "No se encontraron productos para esta tienda con los filtros aplicados"
};
}

return products;
return {
products: products.map(mapStoreProductPricing),
totalProducts
};

} catch (error) {
if (error.status) {
Expand Down Expand Up @@ -890,4 +967,4 @@ export const getStoresService = async (filters = {}) => {
});

return stores;
};
};
7 changes: 7 additions & 0 deletions src/modules/commerce/products/product.controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,13 @@ export const compareProducts = async (request, response) => {
name: p.name,
description: p.description,
price: Number(p.price),
originalPrice:
p.original_price === undefined ? Number(p.price) : Number(p.original_price),
offerPrice:
p.offer_price === undefined || p.offer_price === null
? null
: Number(p.offer_price),
isOffer: Boolean(p.is_offer),
store: p.store
? {
id: p.store.id_store,
Expand Down
Loading
Loading