user()->id; $query = PromotionalCampaign::ofUser($userId) ->withCount('products'); // Filtro por status if ($request->has('status')) { switch ($request->status) { case 'active': $query->active()->current(); break; case 'scheduled': $query->active()->upcoming(); break; case 'ended': $query->where('end_date', '<', Carbon::now()); break; case 'inactive': $query->where('is_active', false); break; } } $campaigns = $query->orderBy('start_date', 'desc')->get()->map(function ($campaign) { return array_merge($campaign->toArray(), [ 'status' => $campaign->status, 'days_remaining' => $campaign->days_remaining, 'is_currently_active' => $campaign->isCurrentlyActive(), ]); }); return response()->json($campaigns); } /** * Cria uma nova campanha */ public function store(Request $request): JsonResponse { $validator = Validator::make($request->all(), [ 'name' => 'required|string|max:255', 'code' => 'nullable|string|max:50', 'description' => 'nullable|string', 'discount_type' => 'required|in:percentage,fixed,price_override', 'discount_value' => 'required|numeric|min:0', 'start_date' => 'required|date', 'end_date' => 'required|date|after_or_equal:start_date', 'start_time' => 'nullable|date_format:H:i', 'end_time' => 'nullable|date_format:H:i', 'is_active' => 'boolean', 'apply_to_all_products' => 'boolean', 'min_price' => 'nullable|numeric|min:0', 'show_original_price' => 'boolean', 'show_discount_badge' => 'boolean', 'badge_text' => 'nullable|string|max:50', 'badge_color' => 'nullable|string|max:20', 'priority' => 'integer|min:0', 'product_ids' => 'nullable|array', 'product_ids.*' => 'exists:product_sheets,id', 'preset' => 'nullable|string', // Para usar presets ]); if ($validator->fails()) { return response()->json(['errors' => $validator->errors()], 422); } $userId = $request->user()->id; $data = $validator->validated(); // Aplicar preset se fornecido if (!empty($data['preset']) && isset(PromotionalCampaign::PRESETS[$data['preset']])) { $preset = PromotionalCampaign::PRESETS[$data['preset']]; $data = array_merge($preset, $data); } unset($data['product_ids'], $data['preset']); $data['user_id'] = $userId; DB::beginTransaction(); try { $campaign = PromotionalCampaign::create($data); // Adicionar produtos se fornecidos if ($request->has('product_ids') && is_array($request->product_ids)) { foreach ($request->product_ids as $productId) { $product = ProductSheet::ofUser($userId)->find($productId); if ($product) { $promoPrice = $campaign->calculatePromotionalPrice($product); $campaign->products()->attach($productId, [ 'promotional_price' => $promoPrice, ]); } } } // Se apply_to_all_products, adicionar todos if ($campaign->apply_to_all_products) { $campaign->applyToAllProducts(); } DB::commit(); $campaign->load('products'); return response()->json(array_merge($campaign->toArray(), [ 'status' => $campaign->status, 'products_count' => $campaign->products->count(), ]), 201); } catch (\Exception $e) { DB::rollBack(); return response()->json(['message' => 'Erro ao criar campanha: ' . $e->getMessage()], 500); } } /** * Exibe uma campanha específica */ public function show(Request $request, $id): JsonResponse { $campaign = PromotionalCampaign::ofUser($request->user()->id) ->with(['products' => function ($query) { $query->select('product_sheets.*') ->withPivot(['discount_type', 'discount_value', 'promotional_price', 'promo_margin', 'promo_margin_percent', 'is_profitable']); }]) ->findOrFail($id); // Adicionar informações de cada produto COM RENTABILIDADE $totalProfit = 0; $unprofitableCount = 0; $productsWithInfo = $campaign->products->map(function ($product) use ($campaign, &$totalProfit, &$unprofitableCount) { $originalPrice = (float) ($product->final_price ?? $product->sale_price); $promoPrice = (float) $product->pivot->promotional_price; $cmv = (float) ($product->cmv_total ?? 0); // Calcular rentabilidade $marginInfo = $campaign->getPromotionalMargin($product); if (!$marginInfo['is_profitable']) { $unprofitableCount++; } $totalProfit += $marginInfo['promo_margin']; return array_merge($product->toArray(), [ 'original_price' => $originalPrice, 'promotional_price' => $promoPrice, 'cmv' => $cmv, 'discount_percent' => $campaign->getDiscountPercentage($originalPrice, $promoPrice), 'savings' => round($originalPrice - $promoPrice, 2), 'badge' => $campaign->getBadgeInfo($originalPrice, $promoPrice), // Dados de rentabilidade 'promo_margin' => $marginInfo['promo_margin'], 'promo_margin_percent' => $marginInfo['promo_margin_percent'], 'original_margin' => $marginInfo['original_margin'], 'original_margin_percent' => $marginInfo['original_margin_percent'], 'margin_reduction' => $marginInfo['margin_reduction'], 'is_profitable' => $marginInfo['is_profitable'], 'is_protected' => $marginInfo['is_protected'], ]); }); return response()->json(array_merge($campaign->toArray(), [ 'status' => $campaign->status, 'days_remaining' => $campaign->days_remaining, 'is_currently_active' => $campaign->isCurrentlyActive(), 'products' => $productsWithInfo, // Resumo de rentabilidade 'profitability_summary' => [ 'total_profit' => round($totalProfit, 2), 'unprofitable_count' => $unprofitableCount, 'all_profitable' => $unprofitableCount === 0, ], ])); } /** * Atualiza uma campanha */ public function update(Request $request, $id): JsonResponse { $campaign = PromotionalCampaign::ofUser($request->user()->id)->findOrFail($id); $validator = Validator::make($request->all(), [ 'name' => 'sometimes|string|max:255', 'code' => 'nullable|string|max:50', 'description' => 'nullable|string', 'discount_type' => 'sometimes|in:percentage,fixed,price_override', 'discount_value' => 'sometimes|numeric|min:0', 'start_date' => 'sometimes|date', 'end_date' => 'sometimes|date|after_or_equal:start_date', 'start_time' => 'nullable|date_format:H:i', 'end_time' => 'nullable|date_format:H:i', 'is_active' => 'boolean', 'apply_to_all_products' => 'boolean', 'min_price' => 'nullable|numeric|min:0', 'show_original_price' => 'boolean', 'show_discount_badge' => 'boolean', 'badge_text' => 'nullable|string|max:50', 'badge_color' => 'nullable|string|max:20', 'priority' => 'integer|min:0', ]); if ($validator->fails()) { return response()->json(['errors' => $validator->errors()], 422); } $campaign->update($validator->validated()); // Recalcular preços se desconto mudou if ($request->hasAny(['discount_type', 'discount_value', 'min_price'])) { $campaign->recalculateAllPrices(); } // Se mudou para apply_to_all_products if ($request->has('apply_to_all_products') && $campaign->apply_to_all_products) { $campaign->applyToAllProducts(); } $campaign->load('products'); return response()->json(array_merge($campaign->toArray(), [ 'status' => $campaign->status, 'products_count' => $campaign->products->count(), ])); } /** * Remove uma campanha */ public function destroy(Request $request, $id): JsonResponse { $campaign = PromotionalCampaign::ofUser($request->user()->id)->findOrFail($id); $campaign->delete(); return response()->json(['message' => 'Campanha excluída com sucesso']); } /** * Adiciona produtos à campanha */ public function addProducts(Request $request, $id): JsonResponse { $campaign = PromotionalCampaign::ofUser($request->user()->id)->findOrFail($id); $validator = Validator::make($request->all(), [ 'product_ids' => 'required|array', 'product_ids.*' => 'exists:product_sheets,id', ]); if ($validator->fails()) { return response()->json(['errors' => $validator->errors()], 422); } $userId = $request->user()->id; $added = 0; foreach ($request->product_ids as $productId) { $product = ProductSheet::ofUser($userId)->find($productId); if ($product && !$campaign->products->contains($productId)) { $promoPrice = $campaign->calculatePromotionalPrice($product); $campaign->products()->attach($productId, [ 'promotional_price' => $promoPrice, ]); $added++; } } return response()->json([ 'message' => "{$added} produto(s) adicionado(s) à campanha", 'products_count' => $campaign->products()->count(), ]); } /** * Remove produtos da campanha */ public function removeProducts(Request $request, $id): JsonResponse { $campaign = PromotionalCampaign::ofUser($request->user()->id)->findOrFail($id); $validator = Validator::make($request->all(), [ 'product_ids' => 'required|array', 'product_ids.*' => 'exists:product_sheets,id', ]); if ($validator->fails()) { return response()->json(['errors' => $validator->errors()], 422); } $campaign->products()->detach($request->product_ids); return response()->json([ 'message' => 'Produto(s) removido(s) da campanha', 'products_count' => $campaign->products()->count(), ]); } /** * Atualiza desconto específico de um produto na campanha */ public function updateProductDiscount(Request $request, $campaignId, $productId): JsonResponse { $campaign = PromotionalCampaign::ofUser($request->user()->id)->findOrFail($campaignId); $validator = Validator::make($request->all(), [ 'discount_type' => 'nullable|in:percentage,fixed,price_override', 'discount_value' => 'nullable|numeric|min:0', ]); if ($validator->fails()) { return response()->json(['errors' => $validator->errors()], 422); } $product = $campaign->products()->where('product_sheet_id', $productId)->first(); if (!$product) { return response()->json(['message' => 'Produto não encontrado na campanha'], 404); } $override = null; if ($request->discount_type) { $override = [ 'discount_type' => $request->discount_type, 'discount_value' => $request->discount_value, ]; } $promoPrice = $campaign->calculatePromotionalPrice($product, $override); $campaign->products()->updateExistingPivot($productId, [ 'discount_type' => $request->discount_type, 'discount_value' => $request->discount_value, 'promotional_price' => $promoPrice, ]); return response()->json([ 'promotional_price' => $promoPrice, 'message' => 'Desconto atualizado', ]); } /** * Retorna presets disponíveis */ public function presets(): JsonResponse { return response()->json(PromotionalCampaign::getPresets()); } /** * Duplica uma campanha */ public function duplicate(Request $request, $id): JsonResponse { $campaign = PromotionalCampaign::ofUser($request->user()->id) ->with('products') ->findOrFail($id); DB::beginTransaction(); try { $newCampaign = $campaign->replicate(); $newCampaign->name = $campaign->name . ' (Cópia)'; $newCampaign->is_active = false; $newCampaign->start_date = Carbon::now(); $newCampaign->end_date = Carbon::now()->addDays(7); $newCampaign->save(); // Copiar produtos foreach ($campaign->products as $product) { $newCampaign->products()->attach($product->id, [ 'discount_type' => $product->pivot->discount_type, 'discount_value' => $product->pivot->discount_value, 'promotional_price' => $product->pivot->promotional_price, ]); } DB::commit(); return response()->json(array_merge($newCampaign->toArray(), [ 'status' => $newCampaign->status, 'products_count' => $newCampaign->products()->count(), ]), 201); } catch (\Exception $e) { DB::rollBack(); return response()->json(['message' => 'Erro ao duplicar: ' . $e->getMessage()], 500); } } /** * Preview de preços para uma campanha COM ANÁLISE DE RENTABILIDADE */ public function preview(Request $request): JsonResponse { $validator = Validator::make($request->all(), [ 'discount_type' => 'required|in:percentage,fixed,price_override', 'discount_value' => 'required|numeric|min:0', 'min_price' => 'nullable|numeric|min:0', 'min_margin_percent' => 'nullable|numeric|min:0', 'protect_against_loss' => 'nullable|boolean', 'product_ids' => 'required|array|min:1', 'product_ids.*' => 'exists:product_sheets,id', ]); if ($validator->fails()) { return response()->json(['errors' => $validator->errors()], 422); } $userId = $request->user()->id; $products = ProductSheet::ofUser($userId) ->whereIn('id', $request->product_ids) ->get(); $protectAgainstLoss = $request->protect_against_loss !== false; // default true $minMarginPercent = $request->min_margin_percent; $unprofitableCount = 0; $protectedCount = 0; $preview = $products->map(function ($product) use ($request, $protectAgainstLoss, $minMarginPercent, &$unprofitableCount, &$protectedCount) { $originalPrice = (float) ($product->final_price ?? $product->sale_price); $cmv = (float) ($product->cmv_total ?? 0); $promoPrice = $originalPrice; // Calcular desconto switch ($request->discount_type) { case 'percentage': $promoPrice = $originalPrice * (1 - ($request->discount_value / 100)); break; case 'fixed': $promoPrice = $originalPrice - $request->discount_value; break; case 'price_override': $promoPrice = $request->discount_value; break; } $wasProtected = false; $protectionReason = null; // Aplicar preço mínimo da campanha if ($request->min_price && $promoPrice < $request->min_price) { $promoPrice = $request->min_price; $wasProtected = true; $protectionReason = 'campaign_min_price'; } // Aplicar preço mínimo do produto if ($product->min_price && $promoPrice < (float) $product->min_price) { $promoPrice = (float) $product->min_price; $wasProtected = true; $protectionReason = 'product_min_price'; } // Proteção contra prejuízo (nunca vender abaixo do CMV) if ($protectAgainstLoss && $cmv > 0 && $promoPrice < $cmv) { $promoPrice = $cmv; $wasProtected = true; $protectionReason = 'cmv_protection'; $protectedCount++; } // Margem mínima obrigatória if ($minMarginPercent && $cmv > 0) { $minPriceWithMargin = $cmv * (1 + ($minMarginPercent / 100)); if ($promoPrice < $minPriceWithMargin) { $promoPrice = $minPriceWithMargin; $wasProtected = true; $protectionReason = 'min_margin'; } } $promoPrice = max(0, round($promoPrice, 2)); // Calcular margens $discountPercent = $originalPrice > 0 ? round((($originalPrice - $promoPrice) / $originalPrice) * 100, 1) : 0; $promoMargin = $promoPrice - $cmv; $promoMarginPercent = $promoPrice > 0 ? round(($promoMargin / $promoPrice) * 100, 1) : 0; $originalMargin = $originalPrice - $cmv; $originalMarginPercent = $originalPrice > 0 ? round(($originalMargin / $originalPrice) * 100, 1) : 0; $isProfitable = $promoMargin > 0; if (!$isProfitable) { $unprofitableCount++; } return [ 'id' => $product->id, 'name' => $product->name, 'sku' => $product->sku, 'cmv' => $cmv, 'original_price' => $originalPrice, 'promotional_price' => $promoPrice, 'discount_percent' => $discountPercent, 'savings' => round($originalPrice - $promoPrice, 2), // Rentabilidade 'promo_margin' => round($promoMargin, 2), 'promo_margin_percent' => $promoMarginPercent, 'original_margin' => round($originalMargin, 2), 'original_margin_percent' => $originalMarginPercent, 'margin_reduction' => round($originalMarginPercent - $promoMarginPercent, 1), 'is_profitable' => $isProfitable, // Proteção 'was_protected' => $wasProtected, 'protection_reason' => $protectionReason, ]; }); $totals = [ 'products_count' => $preview->count(), 'total_original' => round($preview->sum('original_price'), 2), 'total_promotional' => round($preview->sum('promotional_price'), 2), 'total_savings' => round($preview->sum('savings'), 2), 'avg_discount' => round($preview->avg('discount_percent'), 1), // Rentabilidade 'total_cmv' => round($preview->sum('cmv'), 2), 'total_promo_margin' => round($preview->sum('promo_margin'), 2), 'avg_promo_margin_percent' => round($preview->avg('promo_margin_percent'), 1), 'avg_original_margin_percent' => round($preview->avg('original_margin_percent'), 1), // Alertas 'unprofitable_count' => $unprofitableCount, 'protected_count' => $protectedCount, 'all_profitable' => $unprofitableCount === 0, ]; return response()->json([ 'products' => $preview, 'totals' => $totals, ]); } }