Refactor product pricing: catalog owns base price, pricing manager owns rules
This commit is contained in:
2
.gitignore
vendored
2
.gitignore
vendored
@@ -38,7 +38,7 @@ yarn-error.log*
|
|||||||
|
|
||||||
# Configuración de desarrollo que puede contener secretos.
|
# Configuración de desarrollo que puede contener secretos.
|
||||||
# Es mejor usar "User Secrets" en desarrollo para las claves.
|
# Es mejor usar "User Secrets" en desarrollo para las claves.
|
||||||
appsettings.Development.json
|
#appsettings.Development.json
|
||||||
|
|
||||||
# Archivos de publicación de Visual Studio
|
# Archivos de publicación de Visual Studio
|
||||||
[Pp]roperties/[Pp]ublish[Pp]rofiles/
|
[Pp]roperties/[Pp]ublish[Pp]rofiles/
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import type { Product, ProductBundleComponent } from '../../types/Product';
|
import type { Product, ProductBundleComponent } from '../../types/Product';
|
||||||
import type { Company } from '../../types/Company';
|
import type { Company } from '../../types/Company';
|
||||||
|
import type { Category } from '../../types/Category';
|
||||||
import { productService } from '../../services/productService';
|
import { productService } from '../../services/productService';
|
||||||
import { X, Save, Layers, Plus, Trash2, AlertCircle, Package } from 'lucide-react';
|
import { X, Save, Layers, Plus, Trash2, AlertCircle, Package } from 'lucide-react';
|
||||||
import { motion, AnimatePresence } from 'framer-motion';
|
import { motion, AnimatePresence } from 'framer-motion';
|
||||||
@@ -8,6 +9,7 @@ import { motion, AnimatePresence } from 'framer-motion';
|
|||||||
interface Props {
|
interface Props {
|
||||||
product: Product | null;
|
product: Product | null;
|
||||||
companies: Company[];
|
companies: Company[];
|
||||||
|
categories: Category[];
|
||||||
allProducts: Product[];
|
allProducts: Product[];
|
||||||
onClose: (refresh?: boolean) => void;
|
onClose: (refresh?: boolean) => void;
|
||||||
}
|
}
|
||||||
@@ -21,11 +23,12 @@ const PRODUCT_TYPES = [
|
|||||||
{ id: 6, code: 'BUNDLE', name: 'Paquete Promocional (Combo)' },
|
{ id: 6, code: 'BUNDLE', name: 'Paquete Promocional (Combo)' },
|
||||||
];
|
];
|
||||||
|
|
||||||
export default function ProductModal({ product, companies, allProducts, onClose }: Props) {
|
export default function ProductModal({ product, companies, categories, allProducts, onClose }: Props) {
|
||||||
const [formData, setFormData] = useState<Partial<Product>>({
|
const [formData, setFormData] = useState<Partial<Product>>({
|
||||||
name: '',
|
name: '',
|
||||||
description: '',
|
description: '',
|
||||||
companyId: 0,
|
companyId: 0,
|
||||||
|
categoryId: undefined,
|
||||||
productTypeId: 4, // Default Physical
|
productTypeId: 4, // Default Physical
|
||||||
basePrice: 0,
|
basePrice: 0,
|
||||||
taxRate: 21,
|
taxRate: 21,
|
||||||
@@ -169,6 +172,16 @@ export default function ProductModal({ product, companies, allProducts, onClose
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-1.5">
|
||||||
|
<label className="text-[10px] font-black text-slate-400 uppercase tracking-widest ml-1">Rubro Asociado</label>
|
||||||
|
<select className="w-full p-3 bg-slate-50 border-2 border-slate-100 rounded-xl outline-none focus:border-blue-500 font-bold text-sm appearance-none"
|
||||||
|
value={formData.categoryId || 0} onChange={e => setFormData({ ...formData, categoryId: Number(e.target.value) || undefined })}
|
||||||
|
>
|
||||||
|
<option value="0">Ninguno (Producto General)</option>
|
||||||
|
{categories.map(c => <option key={c.id} value={c.id}>{c.name}</option>)}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="space-y-1.5">
|
<div className="space-y-1.5">
|
||||||
<label className="text-[10px] font-black text-slate-400 uppercase tracking-widest ml-1">Precio Base ($)</label>
|
<label className="text-[10px] font-black text-slate-400 uppercase tracking-widest ml-1">Precio Base ($)</label>
|
||||||
<input required type="number" step="0.01" className="w-full p-3 bg-slate-50 border-2 border-slate-100 rounded-xl outline-none focus:border-blue-500 font-black text-sm"
|
<input required type="number" step="0.01" className="w-full p-3 bg-slate-50 border-2 border-slate-100 rounded-xl outline-none focus:border-blue-500 font-black text-sm"
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import { Save, DollarSign, FileText, Type, AlertCircle } from 'lucide-react';
|
|||||||
import { processCategories, type FlatCategory } from '../../utils/categoryTreeUtils';
|
import { processCategories, type FlatCategory } from '../../utils/categoryTreeUtils';
|
||||||
|
|
||||||
interface PricingConfig {
|
interface PricingConfig {
|
||||||
basePrice: number;
|
|
||||||
baseWordCount: number;
|
baseWordCount: number;
|
||||||
extraWordPrice: number;
|
extraWordPrice: number;
|
||||||
specialChars: string;
|
specialChars: string;
|
||||||
@@ -15,7 +14,7 @@ interface PricingConfig {
|
|||||||
|
|
||||||
// Configuración por defecto
|
// Configuración por defecto
|
||||||
const defaultConfig: PricingConfig = {
|
const defaultConfig: PricingConfig = {
|
||||||
basePrice: 0, baseWordCount: 15, extraWordPrice: 0,
|
baseWordCount: 15, extraWordPrice: 0,
|
||||||
specialChars: '!', specialCharPrice: 0, boldSurcharge: 0, frameSurcharge: 0
|
specialChars: '!', specialCharPrice: 0, boldSurcharge: 0, frameSurcharge: 0
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -113,15 +112,9 @@ export default function PricingManager() {
|
|||||||
</h3>
|
</h3>
|
||||||
|
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div>
|
<p className="text-xs text-blue-600 mb-2 bg-blue-50/50 p-2 rounded-lg border border-blue-100">
|
||||||
<label className="block text-sm font-medium text-gray-600 mb-1">Precio Mínimo ($)</label>
|
El Precio Base (Precio Mínimo) ahora se define directamente en los Productos del Catálogo.
|
||||||
<div className="relative">
|
</p>
|
||||||
<span className="absolute left-3 top-2 text-gray-500">$</span>
|
|
||||||
<input type="number" className="pl-6 border p-2 rounded w-full focus:ring-2 focus:ring-blue-500 outline-none"
|
|
||||||
value={config.basePrice} onChange={e => setConfig({ ...config, basePrice: parseFloat(e.target.value) })} />
|
|
||||||
</div>
|
|
||||||
<p className="text-xs text-gray-400 mt-1">Costo por el aviso básico por día.</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="grid grid-cols-2 gap-4">
|
<div className="grid grid-cols-2 gap-4">
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@@ -2,14 +2,17 @@ import { useState, useEffect } from 'react';
|
|||||||
import { Plus, Search, Edit, Box, Layers } from 'lucide-react';
|
import { Plus, Search, Edit, Box, Layers } from 'lucide-react';
|
||||||
import { productService } from '../../../../counter-panel/src/services/productService';
|
import { productService } from '../../../../counter-panel/src/services/productService';
|
||||||
import { companyService } from '../../../../counter-panel/src/services/companyService';
|
import { companyService } from '../../../../counter-panel/src/services/companyService';
|
||||||
|
import { categoryService } from '../../services/categoryService';
|
||||||
import type { Product } from '../../../../counter-panel/src/types/Product';
|
import type { Product } from '../../../../counter-panel/src/types/Product';
|
||||||
import type { Company } from '../../../../counter-panel/src/types/Company';
|
import type { Company } from '../../../../counter-panel/src/types/Company';
|
||||||
|
import type { Category } from '../../types/Category';
|
||||||
import ProductModal from '../../components/Products/ProductModal';
|
import ProductModal from '../../components/Products/ProductModal';
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
|
|
||||||
export default function ProductManager() {
|
export default function ProductManager() {
|
||||||
const [products, setProducts] = useState<Product[]>([]);
|
const [products, setProducts] = useState<Product[]>([]);
|
||||||
const [companies, setCompanies] = useState<Company[]>([]);
|
const [companies, setCompanies] = useState<Company[]>([]);
|
||||||
|
const [categories, setCategories] = useState<Category[]>([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [searchTerm, setSearchTerm] = useState('');
|
const [searchTerm, setSearchTerm] = useState('');
|
||||||
|
|
||||||
@@ -24,12 +27,14 @@ export default function ProductManager() {
|
|||||||
const loadData = async () => {
|
const loadData = async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
const [prodRes, compRes] = await Promise.all([
|
const [prodRes, compRes, catRes] = await Promise.all([
|
||||||
productService.getAll(),
|
productService.getAll(),
|
||||||
companyService.getAll()
|
companyService.getAll(),
|
||||||
|
categoryService.getAll()
|
||||||
]);
|
]);
|
||||||
setProducts(prodRes);
|
setProducts(prodRes);
|
||||||
setCompanies(compRes);
|
setCompanies(compRes);
|
||||||
|
setCategories(catRes);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error cargando catálogo", error);
|
console.error("Error cargando catálogo", error);
|
||||||
} finally {
|
} finally {
|
||||||
@@ -150,6 +155,7 @@ export default function ProductManager() {
|
|||||||
<ProductModal
|
<ProductModal
|
||||||
product={editingProduct}
|
product={editingProduct}
|
||||||
companies={companies}
|
companies={companies}
|
||||||
|
categories={categories}
|
||||||
allProducts={products} // Pasamos todos los productos para poder armar combos
|
allProducts={products} // Pasamos todos los productos para poder armar combos
|
||||||
onClose={handleModalClose}
|
onClose={handleModalClose}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ export interface Product {
|
|||||||
id: number;
|
id: number;
|
||||||
companyId: number;
|
companyId: number;
|
||||||
productTypeId: number; // 1:Classified, 2:Graphic, 3:Radio, 4:Physical, 5:Service, 6:Bundle
|
productTypeId: number; // 1:Classified, 2:Graphic, 3:Radio, 4:Physical, 5:Service, 6:Bundle
|
||||||
|
categoryId?: number; // Para relacionarlo a un rubro
|
||||||
name: string;
|
name: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
sku?: string;
|
sku?: string;
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ interface AdEditorModalProps {
|
|||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
onConfirm: (listingId: number, price: number, description: string) => void;
|
onConfirm: (listingId: number, price: number, description: string) => void;
|
||||||
clientId: number | null; // El aviso se vinculará a este cliente
|
clientId: number | null; // El aviso se vinculará a este cliente
|
||||||
|
productId: number; // Necesario para el precio base
|
||||||
}
|
}
|
||||||
|
|
||||||
interface PricingResult {
|
interface PricingResult {
|
||||||
@@ -18,7 +19,7 @@ interface PricingResult {
|
|||||||
details: string;
|
details: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function AdEditorModal({ isOpen, onClose, onConfirm, clientId }: AdEditorModalProps) {
|
export default function AdEditorModal({ isOpen, onClose, onConfirm, clientId, productId }: AdEditorModalProps) {
|
||||||
const [flatCategories, setFlatCategories] = useState<FlatCategory[]>([]);
|
const [flatCategories, setFlatCategories] = useState<FlatCategory[]>([]);
|
||||||
const [operations, setOperations] = useState<any[]>([]);
|
const [operations, setOperations] = useState<any[]>([]);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
@@ -72,6 +73,7 @@ export default function AdEditorModal({ isOpen, onClose, onConfirm, clientId }:
|
|||||||
try {
|
try {
|
||||||
const res = await api.post('/pricing/calculate', {
|
const res = await api.post('/pricing/calculate', {
|
||||||
categoryId: parseInt(categoryId),
|
categoryId: parseInt(categoryId),
|
||||||
|
productId: productId,
|
||||||
text: debouncedText,
|
text: debouncedText,
|
||||||
days: days,
|
days: days,
|
||||||
isBold: styles.isBold,
|
isBold: styles.isBold,
|
||||||
|
|||||||
@@ -240,6 +240,7 @@ export default function FastEntryPage() {
|
|||||||
try {
|
try {
|
||||||
const res = await api.post('/pricing/calculate', {
|
const res = await api.post('/pricing/calculate', {
|
||||||
categoryId: parseInt(formData.categoryId),
|
categoryId: parseInt(formData.categoryId),
|
||||||
|
productId: 0, // En FastEntry no hay producto aún
|
||||||
text: debouncedText || "",
|
text: debouncedText || "",
|
||||||
days: formData.days,
|
days: formData.days,
|
||||||
isBold: options.isBold,
|
isBold: options.isBold,
|
||||||
|
|||||||
@@ -310,6 +310,7 @@ export default function UniversalPosPage() {
|
|||||||
onClose={() => setShowAdEditor(false)}
|
onClose={() => setShowAdEditor(false)}
|
||||||
onConfirm={handleAdConfirmed}
|
onConfirm={handleAdConfirmed}
|
||||||
clientId={clientId || 1005}
|
clientId={clientId || 1005}
|
||||||
|
productId={selectedAdProduct?.id || 0}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ export interface Product {
|
|||||||
id: number;
|
id: number;
|
||||||
companyId: number;
|
companyId: number;
|
||||||
productTypeId: number; // 1:Classified, 2:Graphic, 3:Radio, 4:Physical, 5:Service, 6:Bundle
|
productTypeId: number; // 1:Classified, 2:Graphic, 3:Radio, 4:Physical, 5:Service, 6:Bundle
|
||||||
|
categoryId?: number; // Para relacionarlo a un rubro
|
||||||
name: string;
|
name: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
sku?: string;
|
sku?: string;
|
||||||
|
|||||||
8
src/SIGCM.API/appsettings.Development.json
Normal file
8
src/SIGCM.API/appsettings.Development.json
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"Logging": {
|
||||||
|
"LogLevel": {
|
||||||
|
"Default": "Information",
|
||||||
|
"Microsoft.AspNetCore": "Warning"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,6 +3,7 @@ namespace SIGCM.Application.DTOs;
|
|||||||
public class CalculatePriceRequest
|
public class CalculatePriceRequest
|
||||||
{
|
{
|
||||||
public int CategoryId { get; set; }
|
public int CategoryId { get; set; }
|
||||||
|
public int ProductId { get; set; } // Añadido para obtener el Precio Base
|
||||||
public required string Text { get; set; }
|
public required string Text { get; set; }
|
||||||
public int Days { get; set; }
|
public int Days { get; set; }
|
||||||
public bool IsBold { get; set; }
|
public bool IsBold { get; set; }
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ public class CategoryPricing
|
|||||||
{
|
{
|
||||||
public int Id { get; set; }
|
public int Id { get; set; }
|
||||||
public int CategoryId { get; set; }
|
public int CategoryId { get; set; }
|
||||||
public decimal BasePrice { get; set; }
|
|
||||||
public int BaseWordCount { get; set; }
|
public int BaseWordCount { get; set; }
|
||||||
public decimal ExtraWordPrice { get; set; }
|
public decimal ExtraWordPrice { get; set; }
|
||||||
public string SpecialChars { get; set; } = "!";
|
public string SpecialChars { get; set; } = "!";
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ public class Product
|
|||||||
public int Id { get; set; }
|
public int Id { get; set; }
|
||||||
public int CompanyId { get; set; }
|
public int CompanyId { get; set; }
|
||||||
public int ProductTypeId { get; set; }
|
public int ProductTypeId { get; set; }
|
||||||
|
public int? CategoryId { get; set; } // Propiedad agregada: Vínculo con Rubro
|
||||||
public required string Name { get; set; }
|
public required string Name { get; set; }
|
||||||
public string? Description { get; set; }
|
public string? Description { get; set; }
|
||||||
public string? SKU { get; set; }
|
public string? SKU { get; set; }
|
||||||
|
|||||||
@@ -121,6 +121,30 @@ END
|
|||||||
|
|
||||||
// --- MIGRACIONES (Schema Update) ---
|
// --- MIGRACIONES (Schema Update) ---
|
||||||
var migrationSql = @"
|
var migrationSql = @"
|
||||||
|
-- Products Columns
|
||||||
|
IF NOT EXISTS(SELECT * FROM sys.columns WHERE Name = N'CategoryId' AND Object_ID = Object_ID(N'Products'))
|
||||||
|
BEGIN
|
||||||
|
ALTER TABLE Products ADD CategoryId INT NULL;
|
||||||
|
-- Agregar clave foránea opcionalmente
|
||||||
|
ALTER TABLE Products ADD CONSTRAINT FK_Products_Categories FOREIGN KEY (CategoryId) REFERENCES Categories(Id);
|
||||||
|
END
|
||||||
|
|
||||||
|
-- CategoryPricing Columns
|
||||||
|
IF EXISTS(SELECT * FROM sys.columns WHERE Name = N'BasePrice' AND Object_ID = Object_ID(N'CategoryPricing'))
|
||||||
|
BEGIN
|
||||||
|
-- Drop the default constraint before dropping the column.
|
||||||
|
-- By finding the constraint dynamically:
|
||||||
|
DECLARE @ConstraintName nvarchar(200)
|
||||||
|
SELECT @ConstraintName = Name FROM sys.default_constraints
|
||||||
|
WHERE parent_object_id = object_id('CategoryPricing')
|
||||||
|
AND parent_column_id = columnproperty(object_id('CategoryPricing'), 'BasePrice', 'ColumnId')
|
||||||
|
|
||||||
|
IF @ConstraintName IS NOT NULL
|
||||||
|
EXEC('ALTER TABLE CategoryPricing DROP CONSTRAINT ' + @ConstraintName)
|
||||||
|
|
||||||
|
ALTER TABLE CategoryPricing DROP COLUMN BasePrice;
|
||||||
|
END
|
||||||
|
|
||||||
-- Listings Columns
|
-- Listings Columns
|
||||||
IF NOT EXISTS(SELECT * FROM sys.columns WHERE Name = N'PublicationStartDate' AND Object_ID = Object_ID(N'Listings'))
|
IF NOT EXISTS(SELECT * FROM sys.columns WHERE Name = N'PublicationStartDate' AND Object_ID = Object_ID(N'Listings'))
|
||||||
ALTER TABLE Listings ADD PublicationStartDate DATETIME2 NULL;
|
ALTER TABLE Listings ADD PublicationStartDate DATETIME2 NULL;
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ public class PricingRepository
|
|||||||
{
|
{
|
||||||
var updateSql = @"
|
var updateSql = @"
|
||||||
UPDATE CategoryPricing
|
UPDATE CategoryPricing
|
||||||
SET BasePrice = @BasePrice, BaseWordCount = @BaseWordCount,
|
SET BaseWordCount = @BaseWordCount,
|
||||||
ExtraWordPrice = @ExtraWordPrice, SpecialChars = @SpecialChars,
|
ExtraWordPrice = @ExtraWordPrice, SpecialChars = @SpecialChars,
|
||||||
SpecialCharPrice = @SpecialCharPrice, BoldSurcharge = @BoldSurcharge,
|
SpecialCharPrice = @SpecialCharPrice, BoldSurcharge = @BoldSurcharge,
|
||||||
FrameSurcharge = @FrameSurcharge
|
FrameSurcharge = @FrameSurcharge
|
||||||
@@ -42,9 +42,9 @@ public class PricingRepository
|
|||||||
{
|
{
|
||||||
var insertSql = @"
|
var insertSql = @"
|
||||||
INSERT INTO CategoryPricing
|
INSERT INTO CategoryPricing
|
||||||
(CategoryId, BasePrice, BaseWordCount, ExtraWordPrice, SpecialChars, SpecialCharPrice, BoldSurcharge, FrameSurcharge)
|
(CategoryId, BaseWordCount, ExtraWordPrice, SpecialChars, SpecialCharPrice, BoldSurcharge, FrameSurcharge)
|
||||||
VALUES
|
VALUES
|
||||||
(@CategoryId, @BasePrice, @BaseWordCount, @ExtraWordPrice, @SpecialChars, @SpecialCharPrice, @BoldSurcharge, @FrameSurcharge)";
|
(@CategoryId, @BaseWordCount, @ExtraWordPrice, @SpecialChars, @SpecialCharPrice, @BoldSurcharge, @FrameSurcharge)";
|
||||||
await conn.ExecuteAsync(insertSql, pricing);
|
await conn.ExecuteAsync(insertSql, pricing);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,8 +47,8 @@ public class ProductRepository : IProductRepository
|
|||||||
{
|
{
|
||||||
using var conn = _db.CreateConnection();
|
using var conn = _db.CreateConnection();
|
||||||
var sql = @"
|
var sql = @"
|
||||||
INSERT INTO Products (CompanyId, ProductTypeId, Name, Description, SKU, BasePrice, TaxRate, IsActive)
|
INSERT INTO Products (CompanyId, ProductTypeId, CategoryId, Name, Description, SKU, BasePrice, TaxRate, IsActive)
|
||||||
VALUES (@CompanyId, @ProductTypeId, @Name, @Description, @SKU, @BasePrice, @TaxRate, @IsActive);
|
VALUES (@CompanyId, @ProductTypeId, @CategoryId, @Name, @Description, @SKU, @BasePrice, @TaxRate, @IsActive);
|
||||||
SELECT CAST(SCOPE_IDENTITY() as int);";
|
SELECT CAST(SCOPE_IDENTITY() as int);";
|
||||||
return await conn.QuerySingleAsync<int>(sql, product);
|
return await conn.QuerySingleAsync<int>(sql, product);
|
||||||
}
|
}
|
||||||
@@ -58,7 +58,7 @@ public class ProductRepository : IProductRepository
|
|||||||
using var conn = _db.CreateConnection();
|
using var conn = _db.CreateConnection();
|
||||||
var sql = @"
|
var sql = @"
|
||||||
UPDATE Products
|
UPDATE Products
|
||||||
SET Name = @Name, Description = @Description, BasePrice = @BasePrice, TaxRate = @TaxRate, IsActive = @IsActive
|
SET CategoryId = @CategoryId, Name = @Name, Description = @Description, BasePrice = @BasePrice, TaxRate = @TaxRate, IsActive = @IsActive
|
||||||
WHERE Id = @Id";
|
WHERE Id = @Id";
|
||||||
await conn.ExecuteAsync(sql, product);
|
await conn.ExecuteAsync(sql, product);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,15 +10,25 @@ public class PricingService
|
|||||||
{
|
{
|
||||||
private readonly PricingRepository _repo;
|
private readonly PricingRepository _repo;
|
||||||
private readonly ICouponRepository _couponRepo;
|
private readonly ICouponRepository _couponRepo;
|
||||||
|
private readonly IProductRepository _productRepo;
|
||||||
|
|
||||||
public PricingService(PricingRepository repo, ICouponRepository couponRepo)
|
public PricingService(PricingRepository repo, ICouponRepository couponRepo, IProductRepository productRepo)
|
||||||
{
|
{
|
||||||
_repo = repo;
|
_repo = repo;
|
||||||
_couponRepo = couponRepo;
|
_couponRepo = couponRepo;
|
||||||
|
_productRepo = productRepo;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<CalculatePriceResponse> CalculateAsync(CalculatePriceRequest request)
|
public async Task<CalculatePriceResponse> CalculateAsync(CalculatePriceRequest request)
|
||||||
{
|
{
|
||||||
|
// 0. Obtener el Producto para saber el Precio Base
|
||||||
|
var product = await _productRepo.GetByIdAsync(request.ProductId);
|
||||||
|
if (product == null) return new CalculatePriceResponse
|
||||||
|
{
|
||||||
|
TotalPrice = 0,
|
||||||
|
Details = "Producto no encontrado."
|
||||||
|
};
|
||||||
|
|
||||||
// 1. Obtener Reglas
|
// 1. Obtener Reglas
|
||||||
var pricing = await _repo.GetByCategoryIdAsync(request.CategoryId);
|
var pricing = await _repo.GetByCategoryIdAsync(request.CategoryId);
|
||||||
|
|
||||||
@@ -44,7 +54,10 @@ public class PricingService
|
|||||||
int realWordCount = words.Length;
|
int realWordCount = words.Length;
|
||||||
|
|
||||||
// 3. Costo Base y Excedente
|
// 3. Costo Base y Excedente
|
||||||
decimal currentCost = pricing.BasePrice; // Precio base incluye N palabras
|
// Obtenemos el precio actual del producto vigenciado, fallback a BasePrice (usando el repo)
|
||||||
|
decimal productBasePrice = await _productRepo.GetCurrentPriceAsync(request.ProductId, request.StartDate == default ? DateTime.UtcNow : request.StartDate);
|
||||||
|
decimal currentCost = productBasePrice; // Precio base incluye N palabras
|
||||||
|
|
||||||
|
|
||||||
// ¿Cuántas palabras extra cobramos?
|
// ¿Cuántas palabras extra cobramos?
|
||||||
// Nota: Los caracteres especiales se cobran aparte según tu requerimiento,
|
// Nota: Los caracteres especiales se cobran aparte según tu requerimiento,
|
||||||
@@ -132,7 +145,7 @@ public class PricingService
|
|||||||
return new CalculatePriceResponse
|
return new CalculatePriceResponse
|
||||||
{
|
{
|
||||||
TotalPrice = Math.Max(0, totalBeforeDiscount - totalDiscount),
|
TotalPrice = Math.Max(0, totalBeforeDiscount - totalDiscount),
|
||||||
BaseCost = pricing.BasePrice * request.Days,
|
BaseCost = productBasePrice * request.Days,
|
||||||
ExtraCost = (extraWordCost + specialCharCost) * request.Days,
|
ExtraCost = (extraWordCost + specialCharCost) * request.Days,
|
||||||
Surcharges = ((request.IsBold ? pricing.BoldSurcharge : 0) + (request.IsFrame ? pricing.FrameSurcharge : 0) + (request.IsFeatured ? featuredSurcharge : 0)) * request.Days,
|
Surcharges = ((request.IsBold ? pricing.BoldSurcharge : 0) + (request.IsFrame ? pricing.FrameSurcharge : 0) + (request.IsFeatured ? featuredSurcharge : 0)) * request.Days,
|
||||||
Discount = totalDiscount,
|
Discount = totalDiscount,
|
||||||
|
|||||||
Reference in New Issue
Block a user