Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
30.10% covered (danger)
30.10%
59 / 196
22.22% covered (danger)
22.22%
4 / 18
CRAP
0.00% covered (danger)
0.00%
0 / 1
SaleController
30.10% covered (danger)
30.10%
59 / 196
22.22% covered (danger)
22.22%
4 / 18
478.59
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 posPrint
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 index
100.00% covered (success)
100.00%
18 / 18
100.00% covered (success)
100.00%
1 / 1
1
 create
0.00% covered (danger)
0.00%
0 / 18
0.00% covered (danger)
0.00%
0 / 1
12
 store
74.36% covered (warning)
74.36%
29 / 39
0.00% covered (danger)
0.00%
0 / 1
11.69
 show
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 edit
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 update
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 addSale
0.00% covered (danger)
0.00%
0 / 19
0.00% covered (danger)
0.00%
0 / 1
20
 DeleteSale
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
1
 saleCartItemUpdate
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
 saleCartItemDelete
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
 saleCartItemEdit
0.00% covered (danger)
0.00%
0 / 15
0.00% covered (danger)
0.00%
0 / 1
2
 destroy
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 saleBackPage
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
2
 salePageCustomer
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 salePageShow
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
2
 addSaleCard
0.00% covered (danger)
0.00%
0 / 38
0.00% covered (danger)
0.00%
0 / 1
30
1<?php
2
3namespace App\Http\Controllers\admin;
4
5use App\Enums\OrderStatus;
6use App\Http\Controllers\Controller;
7use App\Models\admin\Customer;
8use App\Models\admin\Product;
9use App\Models\admin\PurchaseDetails;
10use App\Models\admin\Stock;
11use App\Models\Brand;
12use App\Models\Category;
13use App\Models\Color;
14use App\Models\Order;
15use App\Models\Origin;
16use App\Models\Payment;
17use App\Models\PaymentType;
18use App\Models\SaleCard;
19use App\Models\Size;
20use App\Repository\SaleCardRepository;
21use App\Service\OrderService;
22use App\Service\StockService;
23use App\Services\Location\BranchContext;
24use App\Services\Stock\InventoryService;
25use Illuminate\Http\Request;
26use Illuminate\Support\Facades\DB;
27use Illuminate\Support\Facades\Log;
28use Illuminate\Validation\Rule;
29use Illuminate\Validation\ValidationException;
30use Response;
31
32class SaleController extends Controller
33{
34    protected $sale_repository;
35
36    public function __construct(SaleCardRepository $saleRepository)
37    {
38        $this->sale_repository = $saleRepository;
39    }
40
41    public function posPrint($id)
42    {
43        app(BranchContext::class)->scope(Order::query())->findOrFail($id);
44        $data['details'] = OrderService::getDetails($id);
45        $data['order'] = app(BranchContext::class)->scope(Order::query())->with('payment')->findOrFail($id);
46
47        return view('admin.sales.print')->with($data);
48        // $html = view('admin.sales.print')->with($data)->render();
49        // return response()->json(['html'=>$html]);
50    }
51
52    /**
53     * Display a listing of the resource.
54     *
55     * @return \Illuminate\Http\Response
56     */
57    public function index()
58    {
59        $orders = app(BranchContext::class)->scope(Order::query())
60            ->with(['customer', 'payment', 'details.stock.product', 'details.stock.brand', 'details.stock.color', 'details.stock.size'])
61            ->latest();
62        $branchOrders = app(BranchContext::class)->scope(Order::query());
63
64        // Summary Statistics for the redesign
65        $today = now()->startOfDay();
66        $thisMonth = now()->startOfMonth();
67
68        // These three used to load every order (ever, unbounded) into PHP just to
69        // add up a couple of columns — fine at a handful of orders, not fine once
70        // the table has years of history. Same math, done as aggregate SQL instead.
71        $data['stat_today_sales'] = (clone $branchOrders)->where('created_at', '>=', $today)
72            ->selectRaw('COALESCE(SUM(total + shipping_cost), 0) as total')->value('total');
73        $data['stat_month_sales'] = (clone $branchOrders)->where('created_at', '>=', $thisMonth)
74            ->selectRaw('COALESCE(SUM(total + shipping_cost), 0) as total')->value('total');
75
76        $paidPerOrder = DB::table('payments')->select('order_id', DB::raw('SUM(paid) as paid'))->groupBy('order_id');
77        $data['stat_total_due'] = (clone $branchOrders)
78            ->leftJoinSub($paidPerOrder, 'paid_per_order', 'paid_per_order.order_id', '=', 'orders.id')
79            ->selectRaw('COALESCE(SUM(GREATEST(orders.total + orders.shipping_cost - COALESCE(paid_per_order.paid, 0), 0)), 0) as due')
80            ->value('due');
81
82        $data['stat_total_orders'] = $orders->count();
83
84        $data['data'] = $orders->paginate(15);
85
86        return view('admin.sales.sale_list')->with($data);
87    }
88
89    /**
90     * Show the form for creating a new resource.
91     *
92     * @return \Illuminate\Http\Response
93     */
94    public function create(Request $request)
95    {
96        //        $data['customers'] = Customer::query()->orderByDesc('id')->pluck('name', 'id');
97        $data['customers'] = Customer::getAll();
98        $data['colors'] = Color::query()->pluck('name', 'id');
99        $data['sizes'] = Size::query()->pluck('name', 'id');
100        $data['origins'] = Origin::query()->pluck('name', 'id');
101        $data['categories'] = Category::query()->pluck('name', 'id');
102        $data['brands'] = Brand::query()->pluck('name', 'id');
103
104        // BUG FIX (self-healing): products created/edited before the
105        // StockService::syncStockForProduct fix (or ones whose sync step got
106        // skipped for any reason) would stay permanently invisible in the POS
107        // unless someone remembered to run `php artisan pos:sync-stock` by
108        // hand. Instead of depending on that, just re-sync every product's
109        // Stock row right here, every time the POS page loads. syncStockForProduct
110        // is idempotent (create-if-missing / update-if-exists, never deletes),
111        // so this is safe to run on every page load and guarantees "all
112        // products with stock show up in the POS" without any manual step.
113        Product::with('variations')->get()->each(function ($product) {
114            StockService::syncStockForProduct($product);
115        });
116
117        // BUG FIX: was plain `Stock::query()->with('product')->get()` — a Stock
118        // row whose product had since been deleted (e.g. via the `import:products`
119        // command, which wipes/reimports `products` but never touched `stocks`)
120        // would still show up here with $stock->product == null, rendering as
121        // "Unnamed product" in the POS. whereHas('product') filters those out.
122        // BUG FIX: eager-loading `product.variations.attributeValues.attribute`
123        // too so the POS blade can derive a real variant label (e.g. "Size: 39
124        // · Color: Black") from a Stock row's `VAR-{id}` sync marker — before
125        // this, the variant picker modal just showed "Standard" for every row
126        // because Stock.color_id/size_id are never populated for variants
127        // created through the current product form (they're a legacy pattern).
128        $data['stocks'] = Stock::query()
129            ->with(['product.variations.attributeValues.attribute'])
130            ->whereHas('product')
131            ->get()->unique('id');
132        $data['paymentTypes'] = PaymentType::query()->pluck('name', 'id');
133        $data['warehouses'] = feature('multi_warehouse') ? app(BranchContext::class)->warehouses()->active()->orderByDesc('is_default')->pluck('name', 'id') : collect();
134
135        // Lets the "+ Add customer" flow (see CustomerController::store) come
136        // back here with the just-created customer preselected, instead of
137        // bouncing through the dead legacy `admin.sale.back` page (which
138        // doesn't pass a `stocks` variable at all and would crash this view).
139        if ($request->filled('customer')) {
140            $data['customer'] = Customer::query()->find($request->query('customer'));
141        }
142
143        return view('admin.sales.create')->with($data);
144    }
145
146    /**
147     * Store a newly created resource in storage.
148     *
149     * @return \Illuminate\Http\Response
150     */
151    public function store(Request $request)
152    {
153        if (! $request->has('customer_id') || $request->input('customer_id') === null) {
154            $request->merge(['customer_id' => 0]);
155        }
156        $request->validate([
157            'customer_id' => 'required|integer',
158            'paid' => 'required|numeric|min:0',
159            'status' => ['required', Rule::in(['Pending', 'Confirmed', 'Processing', 'Shipped', 'Delivered'])],
160            'stock_id' => 'required|array|min:1',
161            'stock_id.*' => 'required|integer|exists:stocks,id',
162            'qty' => 'required|array|min:1',
163            'qty.*' => 'required|integer|min:1',
164            'deliver_date' => 'nullable|date',
165            'payment_type_id' => 'required|exists:payment_types,id',
166            'shipping_cost' => 'nullable|numeric|min:0',
167            'note' => 'nullable|string',
168            'order_date' => 'nullable|date',
169            'warehouse_id' => feature('multi_warehouse') ? 'required|exists:warehouses,id' : 'nullable',
170        ]);
171        try {
172            DB::beginTransaction();
173            app(BranchContext::class)->ensureWarehouse($request->integer('warehouse_id') ?: null);
174            if (isset($request->customer_id)) {
175                $customer = $request->customer_id;
176                $order = OrderService::posStoreOrder($customer);
177                $payment = OrderService::posPaymentStore($order);
178
179                // FIX: this used to build $data from the request (status, note,
180                // deliver_date) and then never actually apply it — the order kept
181                // whatever the DB default was (status='Pending') no matter what
182                // the cashier picked on the form, and note/deliver_date had no
183                // columns to be saved into at all (added in the accompanying
184                // migration). Now the cashier's choices are genuinely persisted.
185                $order->update([
186                    'status' => in_array($request->status, OrderStatus::values(), true) ? $request->status : OrderStatus::Pending->value,
187                    'note' => $request->note,
188                    'deliver_date' => $request->deliver_date,
189                ]);
190
191                DB::commit();
192
193                return redirect()->route('admin.order.details', $order->id)->with('success', 'Sale created successfully.');
194            }
195        } catch (\Throwable $e) {
196            DB::rollBack();
197
198            Log::error('Sale store failed: ' . $e->getMessage(), ['exception' => $e]);
199
200            // FIX: this used to show the same generic "something went wrong" for
201            // every failure, including deliberate business-rule rejections (e.g.
202            // OrderService::posStoreOrder now throws a RuntimeException with the
203            // exact product/quantity when stock runs short) — the cashier had no
204            // way to know WHICH item was the problem. Surface that message
205            // verbatim; keep the generic message for genuine unexpected errors
206            // (DB failures, etc.) so those never leak internals to the user.
207            $message = $e instanceof ValidationException
208                ? collect($e->errors())->flatten()->first()
209                : ($e instanceof \RuntimeException
210                ? $e->getMessage()
211                : 'Something went wrong while creating the sale. Please try again.');
212
213            return redirect()->back()->withInput()->with('error', $message);
214        }
215    }
216
217    /**
218     * Display the specified resource.
219     *
220     * @param  int  $id
221     * @return \Illuminate\Http\Response
222     */
223    public function show($id)
224    {
225        //
226    }
227
228    /**
229     * Show the form for editing the specified resource.
230     *
231     * @param  int  $id
232     * @return \Illuminate\Http\Response
233     */
234    public function edit($id)
235    {
236        //
237    }
238
239    /**
240     * Update the specified resource in storage.
241     *
242     * @param  int  $id
243     * @return \Illuminate\Http\Response
244     */
245    public function update(Request $request, $id)
246    {
247        //
248    }
249
250    public function addSale(Request $req)
251    {
252
253        /**For product price, when select a product from select option then return this product price*/
254        if ($req->select_product) {
255            $price = $this->sale_repository->PruductQuery($req->p_query);
256            //            $stockGroup = $this->sale_repository->stockGroup($req->p_query);
257            $brand = $this->sale_repository->brandCheck($req->p_query);
258            $color = $this->sale_repository->colorCheck($req->p_query);
259            $size = $this->sale_repository->sizeCheck($req->p_query);
260            $totalStock = Stock::query()->where('product_id', $req->p_query)->get();
261            $stock = $totalStock->sum('stock_in') - $totalStock->sum('stock_out');
262
263            return response()->json(['price' => $price, 'brand' => $brand, 'color' => $color, 'size' => $size, 'stock' => $stock]);
264        }
265
266        /**
267         * for Customer Details , when a customer select from drop down select option then return those customer details
268         * Customer old data or previews data collecte from SaleCard and previews total
269         * this section work when customer name select,
270         */
271        if ($req->customers) {
272
273            $totalPrice = SaleCard::query()->where('customer_id', $req->customers)->sum('total_price');
274            $previews = $this->sale_repository->customer_previews_sale_card($req->customers);
275            $customer_details = $this->sale_repository->customer_details($req->customers);
276
277            return response()->json(['success' => $previews, 'customer' => $customer_details, 'total' => $totalPrice]);
278        }
279
280        /*
281         * this seciton created for save temposery customer product data in SaleCard
282         * when find product_id and customer_id then this section work successfully
283         */
284
285        if ($req->customer_id) {
286            /*
287             * when found duplicate product in sale card
288             * then alert a message product already added in sale card
289             */
290
291            //            if (SaleCard::query()
292            //                ->where('product_id', $req->product_id)
293            //                ->where('customer_id', $req->customer_id)
294            //                ->where('size_id',$req->size_id)
295            //                ->where('color_id',$req->color_id)
296            //                ->where('brand_id',$req->brand_id)
297            //                ->exists()
298            //                )
299            //            {
300            //               $qty =  SaleCard::query()->update(['qty'=>$req->qty]);
301            //                return response()->json(['qty' => ]);
302            //            }
303
304            /*if product not found in database
305             *Model SaleCard then product add to SaleCard
306            */
307            $save_data = $this->sale_repository->save_product_saleCard($req->all());
308
309            /*
310             * load recent store data in blade file,
311             */
312            $recent = SaleCard::query()->where('id', $save_data->id)->with(['customer', 'product'])->first();
313            $html_data = $this->sale_repository->load_recent_add_product($recent);
314            $totalPrice = SaleCard::query()->where('customer_id', $recent->customer_id)->sum('total_price');
315
316            return response()->json(['success' => $html_data, 'total' => $totalPrice]);
317        }
318    }
319
320    public function DeleteSale(Request $request)
321    {
322        $order = app(BranchContext::class)->scope(Order::query())->with('details.stock')->findOrFail($request->id);
323
324        DB::transaction(function () use ($order) {
325            // Put the sold quantity back — deleting a sale used to just drop the
326            // order + cascade its line items, leaving the stock_out increment
327            // from sale time permanently overstating consumed stock.
328            app(InventoryService::class)->restoreOrder($order);
329
330            Payment::where('order_id', $order->id)->delete();
331            $order->delete();
332        });
333
334        return response()->json(['success' => 'Order deleted successfully', 'grand_total' => 0]);
335    }
336
337    public function saleCartItemUpdate(Request $request)
338    {
339        $data = $request->only(['product_id', 'brand_id', 'color_id', 'size_id', 'qty']);
340        $update = SaleCard::query()->findOrFail($request->cart_id)->update($data);
341        session()->put('customer', $request->customer_id);
342
343        return redirect()->route('admin.sale.back');
344
345    }
346
347    public function saleCartItemDelete(Request $request)
348    {
349        $cart = SaleCard::query()->where('id', $request->id)->delete();
350        session()->put('customer', $request->customer_id);
351        session()->flash('success', 'Cart item successfully deleted');
352
353        return response()->json(['success', 'Cart item successfully deleted']);
354    }
355
356    /**
357     * Remove the specified resource from storage.
358     *
359     * @param  int  $id
360     * @return \Illuminate\Http\Response
361     */
362    public function saleCartItemEdit(Request $request)
363    {
364        $product = SaleCard::query()->findOrFail($request->id);
365        $id = $product->product_id;
366        $data['item'] = Product::query()->findOrFail($id);
367        $product = PurchaseDetails::query()->where('product_id', $id)->first();
368        $data['products'] = PurchaseDetails::query()->with(['product'])->get();
369        $data['cart'] = SaleCard::query()->findOrFail($request->id);
370        $data['price'] = $product->selling_price;
371        $data['brand'] = Stock::query()->where('product_id', $id)->with(['brand'])->get()->unique('brand_id');
372        $data['color'] = Stock::query()->where('product_id', $id)->with(['color'])->get()->unique('color_id');
373        $data['size'] = Stock::query()->where('product_id', $id)->with(['size'])->get()->unique('size_id');
374        $data['totalStock'] = Stock::query()->where('product_id', $id)->get();
375        $data['stock'] = $data['totalStock']->sum('stock_in') - $data['totalStock']->sum('stock_out');
376        $data['customer'] = Customer::query()->findOrFail($request->customer_id);
377        $editForm = view('admin.sales.sale-edit-form')->with($data)->render();
378
379        return response()->json(['form' => $editForm]);
380    }
381
382    public function destroy($id)
383    {
384        //
385    }
386
387    public function saleBackPage()
388    {
389        $id = session()->get('customer');
390        $data['totalPrice'] = SaleCard::query()->where('customer_id', $id)->sum('total_price');
391        $data['customer'] = Customer::query()->where('id', $id)->first();
392        //        $data['customers'] = Customer::query()->orderByDesc('id')->pluck('name', 'id');
393        $data['customers'] = Customer::getAll();
394        $data['colors'] = Color::query()->pluck('name', 'id');
395        $data['sizes'] = Size::query()->pluck('name', 'id');
396        $data['origins'] = Origin::query()->pluck('name', 'id');
397        $data['categories'] = Category::query()->pluck('name', 'id');
398        $data['brands'] = Brand::query()->pluck('name', 'id');
399        $data['purchases'] = PurchaseDetails::query()->with('product')->get();
400        $data['paymentTypes'] = PaymentType::query()->pluck('name', 'id');
401        $data['products'] = SaleCard::query()->where('customer_id', $id)->get();
402
403        return view('admin.sales.create')->with($data);
404    }
405
406    public function salePageCustomer(Request $request)
407    {
408        $customer = Customer::query()->create($request->all());
409
410        return redirect()->route('admin.sale.page', $customer->id);
411    }
412
413    public function salePageShow($id)
414    {
415        $data['customers'] = Customer::query()->pluck('name', 'id');
416        $data['colors'] = Color::query()->pluck('name', 'id');
417        $data['sizes'] = Size::query()->pluck('name', 'id');
418        $data['origins'] = Origin::query()->pluck('name', 'id');
419        $data['categories'] = Category::query()->pluck('name', 'id');
420        $data['brands'] = Brand::query()->pluck('name', 'id');
421        $data['purchases'] = PurchaseDetails::query()->with('product')->get();
422        $data['paymentTypes'] = PaymentType::query()->pluck('name', 'id');
423        $data['customer'] = Customer::query()->findOrFail($id);
424
425        return view('admin.sales.create')->with($data);
426    }
427
428    public function addSaleCard(Request $request)
429    {
430        $data = $request->validate([
431            'customer_id' => 'required',
432            'product_id' => 'required',
433            'brand_id' => 'required',
434            'size_id' => 'required',
435            'color_id' => 'required',
436        ]);
437        try {
438            DB::beginTransaction();
439
440            if ($request->qty <= 0) {
441                return redirect()->back()->with('failed', 'Please Enter quantity less  then one');
442            }
443
444            $saleCheck = SaleCard::query()
445                ->where('product_id', $request->product_id)
446                ->where('customer_id', $request->customer_id)
447                ->where('color_id', $request->color_id)
448                ->where('brand_id', $request->brand_id)
449                ->where('size_id', $request->size_id)
450                ->first();
451
452            if ($saleCheck != null && $saleCheck->exists() == true) {
453                $data['save_data'] = $saleCheck->update(['qty' => $request->qty + $saleCheck->qty]);
454            } else {
455                $data['save_data'] = $this->sale_repository->save_product_saleCard($request->all());
456            }
457            $data['totalPrice'] = SaleCard::query()->where('customer_id', $request->customer_id)->sum('total_price');
458            $data['customer'] = Customer::query()->where('id', $request->customer_id)->first();
459            //            $data['customers'] = Customer::query()->orderByDesc('id')->pluck('name', 'id');
460            $data['customers'] = Customer::getAll();
461            $data['colors'] = Color::query()->pluck('name', 'id');
462            $data['sizes'] = Size::query()->pluck('name', 'id');
463            $data['origins'] = Origin::query()->pluck('name', 'id');
464            $data['categories'] = Category::query()->pluck('name', 'id');
465            $data['brands'] = Brand::query()->pluck('name', 'id');
466            $data['purchases'] = PurchaseDetails::query()->with('product')->get();
467            $data['paymentTypes'] = PaymentType::query()->pluck('name', 'id');
468            $data['products'] = SaleCard::query()->where('customer_id', $request->customer_id)->get();
469
470            DB::commit();
471            session()->put('customer', $request->customer_id);
472
473            return redirect()->route('admin.sale.back');
474            //            return view('admin.sales.create')->with($data);
475
476        } catch (\Throwable $e) {
477            DB::rollBack();
478            Log::error('Sale card add failed: ' . $e->getMessage(), ['exception' => $e]);
479
480            return redirect()->back()->withInput()->with('error', 'Something went wrong while adding the item. Please try again.');
481        }
482    }
483}