<?php /** @noinspection ALL */
/** @noinspection SpellCheckingInspection */
/** @noinspection DuplicatedCode */

namespace Modules\TPConnector\Http\Controllers;

use App\Exceptions\PurchaseSellMismatch;
use App\PurchaseLine;
use App\Transaction;
use App\TransactionSellLinesPurchaseLines;
use App\Variation;
use Carbon\Carbon;
use Exception;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use Illuminate\Http\Request;
use Illuminate\Routing\Controller;
use Illuminate\Support\Facades\Auth;
use App\Utils\BusinessUtil;
use App\Utils\CashRegisterUtil;
use App\Utils\ContactUtil;
use App\Utils\NotificationUtil;
use App\Utils\ProductUtil;
use App\Utils\TransactionUtil;
use App\BusinessLocation;
use App\Product;
use App\TaxRate;
use App\Unit;
use App\Contact;
use App\Business;
use App\TransactionSellLine;
use App\TransactionPayment;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;

/**
 * @group Sales management
 * @authenticated
 *
 * APIs for managing sales
 */
class SellStoreController extends ApiController
{
    /**
     * All Utils instance.
     */
    protected $contactUtil;
    protected $productUtil;
    protected $businessUtil;
    protected $transactionUtil;
    protected $cashRegisterUtil;
    protected $moduleUtil;
    protected $notificationUtil;
    protected $dummyPaymentLine;

    /**
     * Constructor
     *
     * @param ContactUtil $contactUtil
     * @param ProductUtil $productUtil
     * @param BusinessUtil $businessUtil
     * @param TransactionUtil $transactionUtil
     * @param CashRegisterUtil $cashRegisterUtil
     * @param NotificationUtil $notificationUtil
     */
    public function __construct(ContactUtil $contactUtil, ProductUtil $productUtil, BusinessUtil $businessUtil, TransactionUtil $transactionUtil, CashRegisterUtil $cashRegisterUtil, NotificationUtil $notificationUtil)
    {
        $this->contactUtil = $contactUtil;
        $this->productUtil = $productUtil;
        $this->businessUtil = $businessUtil;
        $this->transactionUtil = $transactionUtil;
        $this->cashRegisterUtil = $cashRegisterUtil;
        $this->notificationUtil = $notificationUtil;
        $this->dummyPaymentLine = [
            'method' => 'cash',
            'amount' => 0,
            'note' => '',
            'card_transaction_number' => '',
            'card_number' => '',
            'card_type' => '',
            'card_holder_name' => '',
            'card_month' => '',
            'card_year' => '',
            'card_security' => '',
            'cheque_number' => '',
            'bank_account_number' => '',
            'is_return' => 0,
            'transaction_no' => ''
        ];
        parent::__construct();
    }

    /**
     * Create sell
     *
     * @bodyParam sells.*.location_id int required id of the business location
     * @bodyParam sells.*.contact_id int required id of the customer
     * @bodyParam sells.*.transaction_date string transaction date format:Y-m-d H:i:s, Example: 2020-07-22 15:48:29
     * @bodyParam sells.*.invoice_no string Invoice number
     * @bodyParam sells.*.source string Source of the invoice Example: api, phone, woocommerce
     * @bodyParam sells.*.status string sale status (final, draft) Example: final
     * @bodyParam sells.*.sub_status string sale sub status ("quotation" for quotation and "proforma" for proforma invoice) Example:null
     * @bodyParam sells.*.is_quotation boolean Is sell quotation (0, 1), If 1 status should be draft Example: 1
     * @bodyParam sells.*.tax_rate_id int id of the tax rate applicable to the sale
     * @bodyParam sells.*.discount_amount float discount amount applicable to the sale Example:10.00
     * @bodyParam sells.*.discount_type string  type of the discount amount (fixed, percentage) Example: fixed
     * @bodyParam sells.*.sale_note string
     * @bodyParam sells.*.staff_note string
     * @bodyParam sells.*.commission_agent int commission agent id
     * @bodyParam sells.*.shipping_details string shipping details Example: Express Delivery
     * @bodyParam sells.*.shipping_address string shipping address
     * @bodyParam sells.*.shipping_status string ('ordered', 'packed', 'shipped', 'delivered', 'cancelled') Example: ordered
     * @bodyParam sells.*.delivered_to string Name of the person recieved the consignment Example:'Mr robin'
     * @bodyParam sells.*.shipping_charges float shipping amount Example:10.0000
     * @bodyParam sells.*.packing_charge float packing charge Example:10
     * @bodyParam sells.*.exchange_rate float exchange rate for the currency used Example: 1
     * @bodyParam sells.*.selling_price_group_id int id of the selling price group
     * @bodyParam sells.*.pay_term_number int pay term value Example:3
     * @bodyParam sells.*.pay_term_type string type of the pay term value ('days', 'months') Example: months
     * @bodyParam sells.*.is_suspend boolean Is suspended sale (0, 1) Example: 0
     * @bodyParam sells.*.is_recurring int whether the invoice is recurring (0, 1) Example: 0
     * @bodyParam sells.*.recur_interval int value of the interval invoice will be regenerated
     * @bodyParam sells.*.recur_interval_type string type of the recur interval ('days', 'months', 'years') Example: months
     * @bodyParam sells.*.subscription_repeat_on int day of the month on which invoice will be generated if recur interval type is months (1-30) Example: 15
     * @bodyParam sells.*.subscription_no string subscription number
     * @bodyParam sells.*.recur_repetitions int total number of invoices to be generated
     * @bodyParam sells.*.rp_redeemed int reward points redeemed
     * @bodyParam sells.*.rp_redeemed_amount float reward point redeemed amount after conversion Example: 13.5000
     * @bodyParam sells.*.types_of_service_id int types of service id
     * @bodyParam sells.*.service_custom_field_1 string types of service custom field 1
     * @bodyParam sells.*.service_custom_field_2 string types of service custom field 2
     * @bodyParam sells.*.service_custom_field_3 string types of service custom field 3
     * @bodyParam sells.*.service_custom_field_4 string types of service custom field 4
     * @bodyParam sells.*.service_custom_field_5 string types of service custom field 5
     * @bodyParam sells.*.service_custom_field_6 string types of service custom field 6
     * @bodyParam sells.*.round_off_amount float round off amount on total payable
     * @bodyParam sells.*.table_id int id of the table
     * @bodyParam sells.*.service_staff_id int id of the service staff assigned to the sale
     * @bodyParam sells.*.change_return float Excess paid amount Example:0.0000
     * @bodyParam sells.*.products array required array of the products for the sale
     * @bodyParam sells.*.payments array payment lines for the sale
     *
     *
     * @bodyParam sells.*.products.*.product_id int required product id Example:17
     * @bodyParam sells.*.products.*.variation_id int required variation id Example:58
     * @bodyParam sells.*.products.*.quantity float required quantity Example: 1
     * @bodyParam sells.*.products.*.unit_price float unit selling price Example:437.5000
     * @bodyParam sells.*.products.*.tax_rate_id int tax rate id applicable on the product Example:null
     * @bodyParam sells.*.products.*.discount_amount float discount amount applicable on the product Example:0.0000
     * @bodyParam sells.*.products.*.discount_type string type of discount amount ('fixed', 'percentage') Example: percentage
     * @bodyParam sells.*.products.*.sub_unit_id int sub unit id
     * @bodyParam sells.*.products.*.note string note for the product
     *
     *
     * @bodyParam sells.*.payments.*.amount float required amount of the payment Example: 453.1300
     * @bodyParam sells.*.payments.*.method string payment methods ('cash', 'card', 'cheque', 'bank_transfer', 'other', 'custom_pay_1', 'custom_pay_2', 'custom_pay_3') Example: cash
     * @bodyParam sells.*.payments.*.account_id int account id
     * @bodyParam sells.*.payments.*.card_number string
     * @bodyParam sells.*.payments.*.card_holder_name string
     * @bodyParam sells.*.payments.*.card_transaction_number string
     * @bodyParam sells.*.payments.*.card_type string
     * @bodyParam sells.*.payments.*.card_month string
     * @bodyParam sells.*.payments.*.card_year string
     * @bodyParam sells.*.payments.*.card_security string
     * @bodyParam sells.*.payments.*.transaction_no_1 string
     * @bodyParam sells.*.payments.*.transaction_no_2 string
     * @bodyParam sells.*.payments.*.transaction_no_3 string
     * @bodyParam sells.*.payments.*.bank_account_number string
     * @bodyParam sells.*.payments.*.note string payment note
     * @bodyParam sells.*.payments.*.cheque_number string
     *
     * @response {
     * "data": [
     * {
     * "id": 6,
     * "business_id": 1,
     * "location_id": 1,
     * "res_table_id": null,
     * "res_waiter_id": null,
     * "res_order_status": null,
     * "type": "sell",
     * "sub_type": null,
     * "status": "final",
     * "is_quotation": 0,
     * "payment_status": "paid",
     * "adjustment_type": null,
     * "contact_id": 4,
     * "customer_group_id": null,
     * "invoice_no": "AS0001",
     * "ref_no": "",
     * "source": null,
     * "subscription_no": null,
     * "subscription_repeat_on": null,
     * "transaction_date": "2018-04-10 13:23:21",
     * "total_before_tax": "770.0000",
     * "tax_id": null,
     * "tax_amount": "0.0000",
     * "discount_type": "percentage",
     * "discount_amount": "0.0000",
     * "rp_redeemed": 0,
     * "rp_redeemed_amount": "0.0000",
     * "shipping_details": null,
     * "shipping_address": null,
     * "shipping_status": null,
     * "delivered_to": null,
     * "shipping_charges": "0.0000",
     * "additional_notes": null,
     * "staff_note": null,
     * "round_off_amount": "0.0000",
     * "final_total": "770.0000",
     * "expense_category_id": null,
     * "expense_for": null,
     * "commission_agent": null,
     * "document": null,
     * "is_direct_sale": 0,
     * "is_suspend": 0,
     * "exchange_rate": "1.000",
     * "total_amount_recovered": null,
     * "transfer_parent_id": null,
     * "return_parent_id": null,
     * "opening_stock_product_id": null,
     * "created_by": 1,
     * "import_batch": null,
     * "import_time": null,
     * "types_of_service_id": null,
     * "packing_charge": null,
     * "packing_charge_type": null,
     * "service_custom_field_1": null,
     * "service_custom_field_2": null,
     * "service_custom_field_3": null,
     * "service_custom_field_4": null,
     * "mfg_parent_production_purchase_id": null,
     * "mfg_wasted_units": null,
     * "mfg_production_cost": "0.0000",
     * "mfg_is_final": 0,
     * "is_created_from_api": 0,
     * "essentials_duration": "0.00",
     * "essentials_duration_unit": null,
     * "essentials_amount_per_unit_duration": "0.0000",
     * "essentials_allowances": null,
     * "essentials_deductions": null,
     * "rp_earned": 0,
     * "repair_completed_on": null,
     * "repair_warranty_id": null,
     * "repair_brand_id": null,
     * "repair_status_id": null,
     * "repair_model_id": null,
     * "repair_defects": null,
     * "repair_serial_no": null,
     * "repair_updates_email": 0,
     * "repair_updates_sms": 0,
     * "repair_checklist": null,
     * "repair_security_pwd": null,
     * "repair_security_pattern": null,
     * "repair_due_date": null,
     * "repair_device_id": null,
     * "order_addresses": null,
     * "is_recurring": 0,
     * "recur_interval": null,
     * "recur_interval_type": null,
     * "recur_repetitions": null,
     * "recur_stopped_on": null,
     * "recur_parent_id": null,
     * "invoice_token": null,
     * "pay_term_number": null,
     * "pay_term_type": null,
     * "pjt_project_id": null,
     * "pjt_title": null,
     * "woocommerce_order_id": null,
     * "selling_price_group_id": null,
     * "created_at": "2018-01-06 07:06:11",
     * "updated_at": "2018-01-06 07:06:11",
     * "invoice_url": "http://local.pos.com/invoice/6dfd77eb80f4976b456128e7f1311c9f",
     * "payment_link": "http://local.pos.com/pay/6dfd77eb80f4976b456128e7f1311c9f",
     * "sell_lines": [
     * {
     * "id": 1,
     * "transaction_id": 6,
     * "product_id": 2,
     * "variation_id": 3,
     * "quantity": 10,
     * "mfg_waste_percent": "0.0000",
     * "quantity_returned": "0.0000",
     * "unit_price_before_discount": "70.0000",
     * "unit_price": "70.0000",
     * "line_discount_type": null,
     * "line_discount_amount": "0.0000",
     * "unit_price_inc_tax": "77.0000",
     * "item_tax": "7.0000",
     * "tax_id": 1,
     * "discount_id": null,
     * "lot_no_line_id": null,
     * "sell_line_note": null,
     * "res_service_staff_id": null,
     * "res_line_order_status": null,
     * "woocommerce_line_items_id": null,
     * "parent_sell_line_id": null,
     * "children_type": "",
     * "sub_unit_id": null,
     * "created_at": "2018-01-06 07:06:11",
     * "updated_at": "2018-01-06 07:06:11"
     * }
     * ],
     * "payment_lines": [
     * {
     * "id": 1,
     * "transaction_id": 6,
     * "business_id": null,
     * "is_return": 0,
     * "amount": "770.0000",
     * "method": "cash",
     * "transaction_no": null,
     * "card_transaction_number": null,
     * "card_number": null,
     * "card_type": "visa",
     * "card_holder_name": null,
     * "card_month": null,
     * "card_year": null,
     * "card_security": null,
     * "cheque_number": null,
     * "bank_account_number": null,
     * "paid_on": "2018-01-09 17:30:35",
     * "created_by": 1,
     * "payment_for": null,
     * "parent_id": null,
     * "note": null,
     * "document": null,
     * "payment_ref_no": null,
     * "account_id": null,
     * "created_at": "2018-01-06 01:36:11",
     * "updated_at": "2018-01-06 01:36:11"
     * }
     * ]
     * }
     * ]
     * }
     */
    public function store(Request $request)
    {
        //TODO::Check customer credit limit
        try {
            $sells = $request->input('sells');
            $user = Auth::user();
            $business_id = $user->business_id;
            $business = Business::find($business_id);
            $commsn_agnt_setting = $business->sales_cmsn_agnt;
            $output = [];
            if (empty($sells) || !is_array($sells))
                throw new Exception("Invalid form data");

            foreach ($sells as $sell_data) {
                try {
                    DB::beginTransaction();
                    $sell_data['business_id'] = $user->business_id;
                    $input = $this->__formatSellData($sell_data);
                    //TODO: temporarily used false to bypass the check, bcz of session issue in can_access_this_location function
                    //Check if location allowed
                    if (false && !$user->can_access_this_location($input['location_id']))
                        throw new Exception("User not allowed to access location with id " . $input['location_id']);
                    if (empty($input['products']))
                        throw new Exception("No products added");
                    $discount = [
                        'discount_type' => $input['discount_type'],
                        'discount_amount' => $input['discount_amount']
                    ];
                    $invoice_total = $this->productUtil->calculateInvoiceTotal($input['products'], $input['tax_rate_id'], $discount, false);
                    if ($commsn_agnt_setting == 'logged_in_user')
                        $input['commission_agent'] = $user->id;
                    $transaction = $this->transactionUtil->createSellTransaction($user->business_id, $input, $invoice_total, $user->id, false);
                    $this->transactionUtil->createOrUpdateSellLines($transaction, $input['products'], $input['location_id'], false, null, [], false);
                    //Add change return
                    $change_return = $this->dummyPaymentLine;
                    $change_return['amount'] = $input['change_return'];
                    $change_return['is_return'] = 1;
                    $input['payment'][] = $change_return;
                    if (!empty($input['payment']) && $transaction->is_suspend == 0) {
                        $this->transactionUtil->createOrUpdatePaymentLines($transaction, $input['payment'], $business_id, $user->id, false);
                    }
                    if ($input['status'] == 'final') {
                        //Check for final and do some processing.
                        //update product stock
                        foreach ($input['products'] as $product) {
                            $decrease_qty = $product['quantity'];
                            if (!empty($product['base_unit_multiplier'])) {
                                $decrease_qty = $decrease_qty * $product['base_unit_multiplier'];
                            }
                            if ($product['enable_stock']) {
                                $this->productUtil->decreaseProductQuantity(
                                    $product['product_id'],
                                    $product['variation_id'],
                                    $input['location_id'],
                                    $decrease_qty
                                );
                            }
                            if ($product['product_type'] == 'combo') {
                                //Decrease quantity of combo as well.
                                $this->productUtil
                                    ->decreaseProductQuantityCombo(
                                        $product['combo'],
                                        $input['location_id']
                                    );
                            }
                        }
                        //Update payment status
                        $this->transactionUtil->updatePaymentStatus($transaction->id, $transaction->final_total);
                        if ($business->enable_rp == 1) {
                            $redeemed = !empty($input['rp_redeemed']) ? $input['rp_redeemed'] : 0;
                            $this->transactionUtil->updateCustomerRewardPoints($transaction->contact_id, $transaction->rp_earned, 0, $redeemed);
                        }
                        //Allocate the quantity from purchase and add mapping of
                        //purchase & sell lines in
                        //transaction_sell_lines_purchase_lines table
                        $business_details = $this->businessUtil->getDetails($business_id);
                        $pos_settings = empty($business_details->pos_settings) ? $this->businessUtil->defaultPosSettings() : json_decode($business_details->pos_settings, true);
                        $business_info = [
                            'id' => $business_id,
                            'accounting_method' => $business->accounting_method,
                            'location_id' => $input['location_id'],
                            'pos_settings' => $pos_settings
                        ];
                        // todo this update
                        $this->mapPurchaseSell($business_info, $transaction->sell_lines, $input['products']);
                        //Auto send notification
                        $this->notificationUtil->autoSendNotification($business_id, 'new_sale', $transaction, $transaction->contact);
                        $client = $this->getClient();
                        $this->transactionUtil->activityLog($transaction, 'added', null, ['from_api' => $client->name]);
                    }
                    $transaction->invoice_url = $this->transactionUtil->getInvoiceUrl($transaction->id, $business_id);
                    $transaction->payment_link = $this->transactionUtil->getInvoicePaymentLink($transaction->id, $business_id);
                    DB::commit();
                    $output[] = $transaction;
                } catch (ModelNotFoundException $e) {
                    DB::rollback();
                    Log::emergency("File:" . $e->getFile() . "Line:" . $e->getLine() . "Message:" . $e->getMessage());
                    $output[] = $this->modelNotFoundExceptionResult($e);
                } catch (Exception $e) {
                    DB::rollback();
                    Log::emergency("File:" . $e->getFile() . "Line:" . $e->getLine() . "Message:" . $e->getMessage());
                    $output[] = $this->otherExceptions($e);
                }
            }
        } catch (Exception $e) {
            DB::rollback();
            Log::emergency("File:" . $e->getFile() . "Line:" . $e->getLine() . "Message:" . $e->getMessage());
            $output[] = $this->otherExceptions($e);
        }
        return $output;
    }

    /**
     * Add a mapping between purchase & sell lines.
     * NOTE: Don't use request variable here, request variable don't exist while adding
     * dummybusiness via command line
     *
     * @param array $business
     * @param array $transaction_lines
     * @param array $products
     * @param boolean $check_expiry = true
     *
     * @return object
     */
    public function mapPurchaseSell($business, $transaction_lines, $products, $check_expiry = true)
    {
        if (empty($transaction_lines)) {
            return false;
        }

        if (!empty($business['pos_settings']) && !is_array($business['pos_settings'])) {
            $business['pos_settings'] = json_decode($business['pos_settings'], true);
        }
        $allow_overselling = !empty($business['pos_settings']['allow_overselling']) ?
            true : false;

        //Set flag to check for expired items during SELLING only.
        $stop_selling_expired = false;
        if ($check_expiry)
            if (session()->has('business') && request()->session()->get('business')['enable_product_expiry'] == 1 && request()->session()->get('business')['on_product_expiry'] == 'stop_selling')
                $stop_selling_expired = true;

        $qty_selling = null;
        foreach ($transaction_lines as $line) {
            //Check if stock is not enabled then no need to assign purchase & sell
            $product = Product::find($line->product_id);
            if ($product->enable_stock != 1) {
                continue;
            }

            $qty_sum_query = $this->transactionUtil->get_pl_quantity_sum_string('PL');

            //Get purchase lines, only for products with enable stock.
            $query = Transaction::join('purchase_lines AS PL', 'transactions.id', '=', 'PL.transaction_id')
                ->where('transactions.business_id', $business['id'])
                ->where('transactions.location_id', $business['location_id'])
                ->whereIn('transactions.type', ['purchase', 'purchase_transfer',
                    'opening_stock', 'production_purchase'])
                ->where('transactions.status', 'received')
                ->whereRaw("( $qty_sum_query ) < PL.quantity")
                ->where('PL.product_id', $line->product_id)
                ->where('PL.variation_id', $line->variation_id);
            $purchase_line_id = optional(optional(array_filter($products, function ($p) use ($line) {
                return $p['quantity'] == $line->quantity && $p['product_id'] == $line->product_id && $p['variation_id'] == $line->variation_id;
            }))[0])['purchase_line_id'];

            //If product expiry is enabled then check for on expiry conditions
            if ($stop_selling_expired && empty($purchase_line_id)) {
                $stop_before = request()->session()->get('business')['stop_selling_before'];
                $expiry_date = \Carbon::today()->addDays($stop_before)->toDateString();
                $query->where(function ($q) use ($expiry_date) {
                    $q->whereNull('PL.exp_date')
                        ->orWhereRaw('PL.exp_date > ?', [$expiry_date]);
                });
            }

            //If lot number present consider only lot number purchase line
            if (!empty($line->lot_no_line_id)) {
                $query->where('PL.id', $line->lot_no_line_id);
            }

            //If purchase_line_id is given consider only that purchase line
            if (!empty($purchase_line_id)) {
                $query->where('PL.id', $purchase_line_id);
            }

            //Sort according to LIFO or FIFO
            if ($business['accounting_method'] == 'lifo') {
                $query = $query->orderBy('transaction_date', 'desc');
            } else {
                $query = $query->orderBy('transaction_date', 'asc');
            }

            $rows = $query->select(
                'PL.id as purchase_lines_id',
                DB::raw("(PL.quantity - ( $qty_sum_query )) AS quantity_available"),
                'PL.quantity_sold as quantity_sold',
                'PL.quantity_adjusted as quantity_adjusted',
                'PL.quantity_returned as quantity_returned',
                'PL.mfg_quantity_used as mfg_quantity_used',
                'transactions.invoice_no'
            )->get();
            $purchase_sell_map = [];

            //Iterate over the rows, assign the purchase line to sell lines.
            $qty_selling = $line->quantity;
            foreach ($rows as $k => $row) {
                $qty_allocated = 0;

                //Check if qty_available is more or equal
                if ($qty_selling <= $row->quantity_available) {
                    $qty_allocated = $qty_selling;
                    $qty_selling = 0;
                } else {
                    $qty_selling = $qty_selling - $row->quantity_available;
                    $qty_allocated = $row->quantity_available;
                }
                //Mapping of purchase
                if ($qty_allocated != 0) {
                    $purchase_sell_map[] = [
                        'sell_line_id' => $line->id,
                        'purchase_line_id' => $row->purchase_lines_id,
                        'quantity' => $qty_allocated,
                        'created_at' => \Carbon::now(),
                        'updated_at' => \Carbon::now()
                    ];

                    //Update purchase line
                    PurchaseLine::query()->where('id', $row->purchase_lines_id)
                        ->increment('quantity_sold', $qty_allocated);
                }

                if ($qty_selling == 0) {
                    break;
                }
            }

            if (!($qty_selling == 0 || is_null($qty_selling))) {
                //If overselling not allowed through exception else create mapping with blank purchase_line_id
                if (!$allow_overselling) {
                    $variation = Variation::find($line->variation_id);
                    $mismatch_name = $product->name;
                    if (!empty($variation->sub_sku)) {
                        $mismatch_name .= ' ' . 'SKU: ' . $variation->sub_sku;
                    }
                    if (!empty($qty_selling)) {
                        $mismatch_name .= ' ' . 'Quantity: ' . abs($qty_selling);
                    }

                    $mismatch_error = trans(
                        "messages.purchase_sell_mismatch_exception",
                        ['product' => $mismatch_name]
                    );
                    if ($stop_selling_expired) {
                        $mismatch_error .= __('lang_v1.available_stock_expired');
                    }

                    $business_name = optional(Business::find($business['id']))->name;
                    $location_name = optional(BusinessLocation::find($business['location_id']))->name;
                    \Log::emergency($mismatch_error . ' Business: ' . $business_name . ' Location: ' . $location_name);
                    throw new PurchaseSellMismatch($mismatch_error);
                } else {
                    //Mapping with no purchase line
                    $purchase_sell_map[] = [
                        'sell_line_id' => $line->id,
                        'purchase_line_id' => 0,
                        'quantity' => $qty_selling,
                        'created_at' => \Carbon::now(),
                        'updated_at' => \Carbon::now()
                    ];
                }
            }

            //Insert the mapping
            if (!empty($purchase_adjustment_map)) {
                TransactionSellLinesPurchaseLines::insert($purchase_adjustment_map);
            }
            if (!empty($purchase_sell_map)) {
                TransactionSellLinesPurchaseLines::insert($purchase_sell_map);
            }
        }
    }

    /**
     * Formats input form data to sell data
     * @param array $data
     * @return array
     */
    private function __formatSellData($data, $transaction = null)
    {
        $business_id = $data['business_id'];
        $location = BusinessLocation::where('business_id', $business_id)
            ->findOrFail($data['location_id']);

        $customer_id = $this->__getValue('contact_id', $data, $transaction, null);
        $contact = Contact::where('business_id', $data['business_id'])
            ->whereIn('type', ['customer', 'both'])
            ->findOrFail($customer_id);

        $cg = $this->contactUtil->getCustomerGroup($business_id, $contact->id);
        $customer_group_id = (empty($cg) || empty($cg->id)) ? null : $cg->id;
        $formated_data = [
            'business_id' => $business_id,
            'location_id' => $location->id,
            'contact_id' => $contact->id,
            'customer_group_id' => $customer_group_id,
            'transaction_date' => $this->__getValue('transaction_date', $data,
                $transaction, Carbon::now()->toDateTimeString()),
            'invoice_no' => $this->__getValue('invoice_no', $data, $transaction, null, 'invoice_no'),
            'source' => $this->__getValue('source', $data, $transaction, null, 'source'),
            'status' => $this->__getValue('status', $data, $transaction, 'final'),
            'sub_status' => $this->__getValue('sub_status', $data, $transaction, null),
            'sale_note' => $this->__getValue('sale_note', $data, $transaction),
            'staff_note' => $this->__getValue('staff_note', $data, $transaction),
            'commission_agent' => $this->__getValue('commission_agent',
                $data, $transaction),
            'shipping_details' => $this->__getValue('shipping_details',
                $data, $transaction),
            'shipping_address' => $this->__getValue('shipping_address',
                $data, $transaction),
            'shipping_status' => $this->__getValue('shipping_status', $data, $transaction),
            'delivered_to' => $this->__getValue('delivered_to', $data, $transaction),
            'shipping_charges' => $this->__getValue('shipping_charges', $data,
                $transaction, 0),
            'exchange_rate' => $this->__getValue('exchange_rate', $data, $transaction, 1),
            'selling_price_group_id' => $this->__getValue('selling_price_group_id', $data, $transaction),
            'pay_term_number' => $this->__getValue('pay_term_number', $data, $transaction),
            'pay_term_type' => $this->__getValue('pay_term_type', $data, $transaction),
            'is_recurring' => $this->__getValue('is_recurring', $data, $transaction, 0),
            'recur_interval' => $this->__getValue('recur_interval', $data, $transaction),
            'recur_interval_type' => $this->__getValue('recur_interval_type', $data, $transaction),
            'is_direct_sale' => $this->__getValue('is_direct_sale', $data, $transaction, 0),
            'subscription_repeat_on' => $this->__getValue('subscription_repeat_on', $data, $transaction),
            'subscription_no' => $this->__getValue('subscription_no', $data, $transaction),
            'recur_repetitions' => $this->__getValue('recur_repetitions', $data, $transaction, 0),
            'order_addresses' => $this->__getValue('order_addresses', $data, $transaction),
            'rp_redeemed' => $this->__getValue('rp_redeemed', $data, $transaction, 0),
            'rp_redeemed_amount' => $this->__getValue('rp_redeemed_amount', $data, $transaction, 0),
            'is_created_from_api' => 1,
            'types_of_service_id' => $this->__getValue('types_of_service_id', $data, $transaction),
            'packing_charge' => $this->__getValue('packing_charge', $data, $transaction, 0),
            'packing_charge_type' => $this->__getValue('packing_charge_type', $data, $transaction),
            'service_custom_field_1' => $this->__getValue('service_custom_field_1', $data, $transaction),
            'service_custom_field_2' => $this->__getValue('service_custom_field_2', $data, $transaction),
            'service_custom_field_3' => $this->__getValue('service_custom_field_3', $data, $transaction),
            'service_custom_field_4' => $this->__getValue('service_custom_field_4', $data, $transaction),
            'service_custom_field_5' => $this->__getValue('service_custom_field_5', $data, $transaction),
            'service_custom_field_6' => $this->__getValue('service_custom_field_6', $data, $transaction),
            'custom_field_1' => $this->__getValue('custom_field_1', $data, $transaction),
            'custom_field_2' => $this->__getValue('custom_field_2', $data, $transaction),
            'custom_field_3' => $this->__getValue('custom_field_3', $data, $transaction),
            'custom_field_4' => $this->__getValue('custom_field_4', $data, $transaction),
            'round_off_amount' => $this->__getValue('round_off_amount', $data, $transaction),
            'res_table_id' => $this->__getValue('table_id', $data, $transaction, null, 'res_table_id'),
            'res_waiter_id' => $this->__getValue('service_staff_id', $data, $transaction, null, 'res_waiter_id'),
            'change_return' => $this->__getValue('change_return', $data, $transaction, 0),
            'change_return_id' => $this->__getValue('change_return_id', $data, $transaction, null),
            'is_quotation' => $this->__getValue('is_quotation', $data, $transaction, 0),
            'is_suspend' => $this->__getValue('is_suspend', $data, $transaction, 0)
        ];

        //Generate reference number
        if (!empty($formated_data['is_recurring'])) {
            //Update reference count
            $ref_count = $this->transactionUtil->setAndGetReferenceCount('subscription', $business_id);
            $formated_data['subscription_no'] = $this->transactionUtil->generateReferenceNumber('subscription', $ref_count, $business_id);
        }

        $sell_lines = [];
        $subtotal = 0;

        if (!empty($data['products'])) {
            foreach ($data['products'] as $product_data) {

                $sell_line = null;
                if (!empty($product_data['sell_line_id'])) {
                    $sell_line = TransactionSellLine::findOrFail($product_data['sell_line_id']);
                }

                $product_id = $this->__getValue('product_id', $product_data, $sell_line);
                $variation_id = $this->__getValue('variation_id', $product_data, $sell_line);
                $product = Product::where('business_id', $business_id)
                    ->with(['variations'])
                    ->findOrFail($product_id);

                $variation = $product->variations->where('id', $variation_id)->first();

                //Calculate line discount
                $unit_price = $this->__getValue('unit_price', $product_data, $sell_line, $variation->sell_price_inc_tax, 'unit_price_before_discount');

                $discount_amount = $this->__getValue('discount_amount', $product_data, $sell_line, 0, 'line_discount_amount');

                $line_discount = $discount_amount;
                $line_discount_type = $this->__getValue('discount_type', $product_data, $sell_line, 'fixed', 'line_discount_type');

                if ($line_discount_type == 'percentage') {
                    $line_discount = $this->transactionUtil->calc_percentage($unit_price, $discount_amount);
                }
                $discounted_price = $unit_price - $line_discount;

                //calculate line tax
                $item_tax = 0;
                $unit_price_inc_tax = $discounted_price;
                $tax_id = $this->__getValue('tax_rate_id', $product_data, $sell_line, null, 'tax_id');
                if (!empty($tax_id)) {
                    $tax = TaxRate::where('business_id', $business_id)
                        ->findOrFail($tax_id);

                    $item_tax = $this->transactionUtil->calc_percentage($discounted_price, $tax->amount);
                    $unit_price_inc_tax += $item_tax;
                }

                $formated_sell_line = [
                    'product_id' => $product->id,
                    'variation_id' => $variation->id,
                    'product_type' => $product->type,
                    'unit_price' => $unit_price,
                    'line_discount_type' => $line_discount_type,
                    'line_discount_amount' => $discount_amount,
                    'tax_id' => $tax_id,
                    'item_tax' => $item_tax,
                    'sell_line_note' => $this->__getValue('note', $product_data, $sell_line, null, 'sell_line_note'),
                    'enable_stock' => $product->enable_stock,
                    'quantity' => $this->__getValue('quantity', $product_data,
                        $sell_line, 0),
                    'product_unit_id' => $product->unit_id,
                    'sub_unit_id' => $this->__getValue('sub_unit_id', $product_data,
                        $sell_line),
                    'unit_price_inc_tax' => $unit_price_inc_tax,
                ];
                if (isset($product_data['lot_number']) && !empty($product_data['lot_number']))
                    $formated_sell_line['purchase_line_id'] = PurchaseLine::query()
                        ->where('product_id', $product_data['product_id'])
                        ->where('variation_id', $product_data['variation_id'])
                        ->where('lot_number', $product_data['lot_number'])
                        ->firstOrFail()->id;
                if (!empty($sell_line)) {
                    $formated_sell_line['transaction_sell_lines_id'] = $sell_line->id;
                }

                if (($formated_sell_line['product_unit_id'] != $formated_sell_line['sub_unit_id']) && !empty($formated_sell_line['sub_unit_id'])) {
                    $sub_unit = Unit::where('business_id', $business_id)
                        ->findOrFail($formated_sell_line['sub_unit_id']);
                    $formated_sell_line['base_unit_multiplier'] = $sub_unit->base_unit_multiplier;
                } else {
                    $formated_sell_line['base_unit_multiplier'] = 1;
                }

                //Combo product
                if ($product->type == 'combo') {
                    $combo_variations = $this->productUtil->calculateComboDetails($location->id, $variation->combo_variations);
                    foreach ($combo_variations as $key => $value) {
                        $combo_variations[$key]['quantity'] = $combo_variations[$key]['qty_required'] * $formated_sell_line['quantity'] * $formated_sell_line['base_unit_multiplier'];
                    }

                    $formated_sell_line['combo'] = $combo_variations;
                }

                $line_total = $unit_price_inc_tax * $formated_sell_line['quantity'];

                $sell_lines[] = $formated_sell_line;

                $subtotal += $line_total;
            }
        }

        $formated_data['products'] = $sell_lines;

        //calculate sell discount and tax
        $order_discount_amount = $this->__getValue('discount_amount', $data, $transaction, 0);
        $order_discount_type = $this->__getValue('discount_type', $data, $transaction, 'fixed');
        $order_discount = $order_discount_amount;
        if ($order_discount_type == 'percentage') {
            $order_discount = $this->transactionUtil->calc_percentage($subtotal, $order_discount_amount);
        }
        $discounted_total = $subtotal - $order_discount;

        //calculate line tax
        $order_tax = 0;
        $final_total = $discounted_total;
        $order_tax_id = $this->__getValue('tax_rate_id', $data, $transaction);
        if (!empty($order_tax_id)) {
            $tax = TaxRate::where('business_id', $business_id)
                ->findOrFail($order_tax_id);

            $order_tax = $this->transactionUtil->calc_percentage($discounted_total, $tax->amount);
            $final_total += $order_tax;
        }

        $formated_data['discount_amount'] = $order_discount_amount;
        $formated_data['discount_type'] = $order_discount_type;
        $formated_data['tax_rate_id'] = $order_tax_id;
        $formated_data['tax_calculation_amount'] = $order_tax;

        $final_total += $formated_data['shipping_charges'];

        if (!empty($formated_data['packing_charge']) && !empty($formated_data['types_of_service_id'])) {
            $final_total += $formated_data['packing_charge'];
        }

        $formated_data['final_total'] = $final_total;

        $payments = [];
        if (!empty($data['payments'])) {
            foreach ($data['payments'] as $payment_data) {
                $transaction_payment = null;
                if (!empty($payment_data['payment_id'])) {
                    $transaction_payment = TransactionPayment::findOrFail($payment_data['payment_id']);
                }
                $payment = [
                    'amount' => $this->__getValue('amount', $payment_data, $transaction_payment),
                    'method' => $this->__getValue('method', $payment_data, $transaction_payment),
                    'account_id' => $this->__getValue('account_id', $payment_data, $transaction_payment),
                    'card_number' => $this->__getValue('card_number', $payment_data, $transaction_payment),
                    'card_holder_name' => $this->__getValue('card_holder_name', $payment_data, $transaction_payment),
                    'card_transaction_number' => $this->__getValue('card_transaction_number', $payment_data, $transaction_payment),
                    'card_type' => $this->__getValue('card_type', $payment_data, $transaction_payment),
                    'card_month' => $this->__getValue('card_month', $payment_data, $transaction_payment),
                    'card_year' => $this->__getValue('card_year', $payment_data, $transaction_payment),
                    'card_security' => $this->__getValue('card_security', $payment_data, $transaction_payment),
                    'cheque_number' => $this->__getValue('cheque_number', $payment_data, $transaction_payment),
                    'bank_account_number' => $this->__getValue('bank_account_number', $payment_data, $transaction_payment),
                    'transaction_no_1' => $this->__getValue('transaction_no_1', $payment_data, $transaction_payment),
                    'transaction_no_2' => $this->__getValue('transaction_no_2', $payment_data, $transaction_payment),
                    'transaction_no_3' => $this->__getValue('transaction_no_3', $payment_data, $transaction_payment),
                    'note' => $this->__getValue('note', $payment_data, $transaction_payment),
                ];
                if (!empty($transaction_payment)) {
                    $payment['payment_id'] = $transaction_payment->id;
                }

                $payments[] = $payment;
            }

            $formated_data['payment'] = $payments;
        }
        return $formated_data;
    }

    private function __getValue($key, $data, $obj, $default = null, $db_key = null)
    {
        $value = $default;

        if (isset($data[$key])) {
            $value = $data[$key];
        } else if (!empty($obj)) {
            $key = !empty($db_key) ? $db_key : $key;
            $value = $obj->$key;
        }

        return $value;
    }
}
