AMS_Backend/app/Services/CalculateService.php
2025-11-06 13:41:06 +08:00

690 lines
29 KiB
PHP

<?
namespace App\Services;
use App\Models\CartItems;
use App\Models\MenuItemVariations;
use App\Models\DeliverySettingModel;
use App\Models\CartItemOptions;
use App\Models\Carts;
use App\Models\Customer;
use App\Models\CustomerVoucherList;
use App\Models\MenuItems;
use App\Models\MenuImages;
use App\Models\OutletMenus;
use App\Models\OutletTax;
use App\Models\StoreDiscount;
use App\Models\SettingTax;
use App\Models\Outlet;
use App\Services\PromoService;
use App\Models\PromoCode;
use App\Libraries\Lalamove;
use App\Models\PwpModel;
use Exception;
use CodeIgniter\Database\Config;
helper('general');
class CalculateService
{
private $cart_items;
private $variations;
private $options;
private $carts;
private $menu_items;
private $menu_images;
private $outlet_menus;
private $outlet_tax;
private $setting_tax;
private $cart_item_options;
private $customer_voucher_list;
private $promo_service;
private $promo_codes;
private $pwp;
private $outlet;
private $store_discount;
private $customer;
private $setting_delivery_fee;
public function __construct(PromoService $promoService)
{
$this->cart_items = new CartItems();
$this->variations = new MenuItemVariations();
$this->options = new CartItemOptions();
$this->carts = new Carts();
$this->menu_items = new MenuItems();
$this->menu_images = new MenuImages();
$this->outlet_menus = new OutletMenus();
$this->outlet_tax = new OutletTax();
$this->setting_tax = new SettingTax();
$this->cart_item_options = new CartItemOptions();
$this->promo_service = $promoService;
$this->promo_codes = new PromoCode();
$this->customer_voucher_list = new CustomerVoucherList();
$this->outlet = new Outlet();
$this->store_discount = new StoreDiscount();
$this->customer = new Customer();
$this->pwp = new PwpModel();
$this->setting_delivery_fee = new DeliverySettingModel();
helper('order_helper');
}
private function getOriginalItemPrice($menu_item_id)
{
$menu_item = $this->menu_items->find($menu_item_id);
return $menu_item ? $menu_item['price'] : 0;
}
private function checkItemPwpDiscount($cart_id)
{
$cart = $this->carts->find($cart_id);
$cart_items = $this->cart_items->where('cart_id', $cart_id)->findAll();
// print_r($cart_items);exit;
$pwp_promos = $this->pwp->where('deleted_at', null)->orderBy('order_index', 'ASC')->findAll();
$total_qualify = 0;
foreach ($pwp_promos as $promo) {
if($promo['mode'] == 'selected_item'){
$requiredIds = explode(',', $promo['pwp_item_id']);
foreach($cart_items as $item){
if(in_array($item['menu_item_id'], $requiredIds)){
// echo(123);exit;
if($promo['amount_type'] == 'amount'){
$total_qualify+=($item['quantity'] * $item['unit_price']);
}else{
$total_qualify+=$item['quantity'];
}
}
}
}else{
if($promo['amount_type'] == 'amount'){
$total_qualify+= $cart['grand_total'];
}else{
$total_qualify+= $cart['item_count'];
}
}
// echo($total_qualify);exit;
if($total_qualify >= $promo['amount']){
return [
'has_discount' => true,
'pwp_item' => explode(',', $promo['selected_item']),
];
}
}
return ['has_discount' => false];
}
public function calculateCartTotals($cart_id, $outlet_id, $selected_date = null, $selected_time = null, $latitude = null, $longitude = null, $order_type = null, $address = null, $promo_or_voucher = null)
{
$cart = $this->carts->find($cart_id);
if ($selected_date == null && $selected_time == null) {
$selected_date = $cart['selected_date'];
$selected_time = $cart['selected_time'];
}
if ($order_type == null) {
$order_type = $cart['order_type'];
}
if ($address == null) {
$address = $cart['address'];
}
if ($latitude == null && $longitude == null) {
$latitude = $cart['latitude'];
$longitude = $cart['longitude'];
}
$cart_items = $this->cart_items->where('cart_id', $cart_id)->findAll();
$customer_id = $cart['customer_id'];
$customer = $this->customer->where('id', $customer_id)->first();
$customer_tier = $customer['customer_tier_id'] ?? 0;
$subtotal = $subtotal_without_options = 0;
$total_discount = $promo_discount_amount = 0;
$total_tax = 0;
$invalid_items = [];
$promo_code_id = $cart['promo_code_id'];
$free_item_list = [];
$promo_code = '';
$voucher_code = '';
$packaging_charge = 0;
$array_discount = [];
$store_discount = $this->store_discount->where("FIND_IN_SET($outlet_id, outlet_list)")->where("FIND_IN_SET($customer_tier, tier_id_list)")->findAll();
// print_r($cart_items);exit;
foreach ($cart_items as $item) {
$is_valid = true;
if ($outlet_id) {
$menu_available = $this->outlet_menus->where([
'menu_item_id' => $item['menu_item_id'],
'outlet_id' => $outlet_id
])->first();
if (!$menu_available) {
$is_valid = false;
$invalid_items[] = $item['id'];
$this->options->where('cart_item_id', $item['id'])->delete();
continue;
}
}
$unit_price = $item['unit_price'];
$title = $item['title'];
$variation_price = 0;
$packaging_price = 0;
if (!empty($item['variation_id']) && $item['variation_id'] > 0) {
$menu_item = $this->menu_items->find($item['menu_item_id']);
$variation = $this->variations->find($item['variation_id']);
if ($variation) {
$unit_price = $variation['price'];
$packaging_price = $menu_item['packaging_price'] ?? 0;
$packaging_charge = $packaging_price * $item['quantity'];
}
} else {
$menu_item = $this->menu_items->find($item['menu_item_id']);
if ($menu_item) {
$unit_price = $item['is_pwp'] == true ? $menu_item['pwp_price'] : $menu_item['price'];
$title = $menu_item['title'];
$packaging_price = $menu_item['packaging_price'] ?? 0;
$packaging_charge = $packaging_price * $item['quantity'];
}
}
if(is_array($store_discount)){
foreach($store_discount as $discount){
$menu_item_list = explode(',', $discount['menu_item_list']);
if(in_array($item['menu_item_id'], $menu_item_list)){
if($discount['discount_type'] == 'percentage'){
$total_discount += ($unit_price * $item['quantity']) * ($discount['discount_value'] / 100);
if(!array_key_exists($discount['id'], $array_discount)){
$array_discount[$discount['id']] = [
'discount_name' => $discount['discount_name'],
'discount_type' => $discount['discount_type'],
'discount_value' => $discount['discount_value'],
'discount_amount' => number_format_no_round(($unit_price * $item['quantity']) * ($discount['discount_value'] / 100))
];
}else{
$array_discount[$discount['id']]['discount_amount'] += number_format_no_round(($unit_price * $item['quantity']) * ($discount['discount_value'] / 100));
}
}else{
if(!array_key_exists($discount['id'], $array_discount)){
$total_discount = $discount['discount_value'];
$array_discount[$discount['id']] = [
'discount_name' => $discount['discount_name'],
'discount_type' => $discount['discount_type'],
'discount_value' => $discount['discount_value'],
'discount_amount' => number_format_no_round(($unit_price * $item['quantity']) * ($discount['discount_value'] / 100))
];
}
}
}
}
}
$item_subtotal = ($unit_price) * $item['quantity'];
$item_subtotal_without_options = $item_subtotal;
$options = $this->options->where('cart_item_id', $item['id'])->findAll();
$options_total = array_sum(array_column($options, 'price_adjustment'));
$item_subtotal += $options_total;
if (($item['unit_price'] != $unit_price || $item['title'] != $title) && (($item['is_free_item'] == false || $item['is_free_item'] == '1') && $item['is_pwp'] == false)) {
$this->cart_items->update($item['id'], [
'unit_price' => $unit_price,
'title' => $title,
'line_subtotal' => $item_subtotal
]);
}
if ($item['is_free_item'] == true || $item['is_free_item'] == '1') {
$item_subtotal = 0;
}
if($item['is_pwp'] == true && ($item['unit_price'] != $unit_price || $item['title'] != $title)){
// $item_subtotal = $item['pwp_price'] * $item['quantity'];
$this->cart_items->update($item['id'], [
'unit_price' => $unit_price,
'title' => $item['title'],
'line_subtotal' => $item_subtotal
]);
}
if ($is_valid && $item['is_free_item'] != true && $item['is_free_item'] != '1') {
$subtotal += $item_subtotal;
$subtotal_without_options += $item_subtotal_without_options;
}
}
if($subtotal < $total_discount){
$total_discount = $subtotal;
}
if (!empty($invalid_items)) {
$this->cart_items->whereIn('id', $invalid_items)->delete();
$cart_items = $this->cart_items->where('cart_id', $cart_id)->findAll();
}
// Get cart items with options (after potential deletions)
$final_cart_items = $this->cart_items
->where('cart_id', $cart_id)
->where('deleted_at', null)
->findAll();
foreach ($final_cart_items as &$item) {
$item['variation'] = $this->variations->find($item['variation_id']);
$item['options'] = $this->options
->where('cart_item_id', $item['id'])
->where('deleted_at', null)
->findAll();
if ($item['variation_id'] > 0) {
$item['image'] = $item['variation']['images'];
} else {
$image = $this->menu_images->where('menu_item_id', $item['menu_item_id'])->orderBy('order_index', 'ASC')->first();
if ($image) {
$item['image'] = 'https://icom.ipsgroup.com.my/backend/uploads/menu_images/'.$image['image_url'];
} else {
$item['image'] = null;
}
}
}
//check store discount
// $store_discount = $this->store_discount->where("FIND_IN_SET($outlet_id, outlet_list)")->where("FIND_IN_SET($order_type, order_type)")->findAll();
// print_r($final_cart_items);exit;
$quotation_id = null;
$delivery_fee = 0;
$delivery_service = null;
if ($order_type == 'delivery' && $latitude && $longitude && $address) {
$outlet_data = $this->outlet->where('id', $outlet_id)->first();
$delivery_options = explode(',', $outlet_data['delivery_options'] ?? '');
// Lalamove
if (in_array('Lalamove', $delivery_options)) {
try {
$lalamove = new Lalamove();
$delivery_response = $lalamove->requestQuotation(
$selected_date,
$selected_time,
$subtotal,
$outlet_id,
$latitude,
$longitude,
$address
);
if (isset($delivery_response['data'])) {
$quotation_id = $delivery_response['data']['quotationId'];
$delivery_fee = $delivery_response['data']['priceBreakdown']['total'];
$delivery_service = 'Lalamove';
}
} catch (Exception $e) {
log_message('error', 'Lalamove quotation failed: ' . $e->getMessage());
}
}
// Grab
if (!$quotation_id && in_array('Grab Express', $delivery_options)) {
try {
$grab = new \App\Libraries\Grab();
$delivery_response = $grab->requestQuotation(
$selected_date,
$selected_time,
$subtotal,
$outlet_id,
$latitude,
$longitude,
$address
);
if (!empty($delivery_response['quotes'])) {
$quotation_id = time();
$delivery_fee = $delivery_response['quotes'][0]['amount'] ?? 0;
$delivery_service = 'Grab';
}
} catch (Exception $e) {
log_message('error', 'Grab quotation failed: ' . $e->getMessage());
}
}
// Manual fallback
if (!$quotation_id) {
$distance = $this->calculateDistance($outlet_id, $latitude, $longitude);
$delivery_fee = $this->calculateDeliveryFee($distance);
}
}
if ($cart['promo_code_id'] > 0 || $cart['customer_voucher_list_id'] > 0) {
$promo_setting_id = 0;
//check promo
if ($cart['promo_code_id'] > 0) {
$promo_data = $this->promo_codes->find($cart['promo_code_id']);
$promo_code = $promo_data['code'];
$promo_setting_id = $promo_data['promo_setting_id'];
} else if ($cart['customer_voucher_list_id'] > 0) {
$voucher_data = $this->customer_voucher_list->where('id', $cart['customer_voucher_list_id'])->first();
$voucher_code = $voucher_data['voucher_code'];
$promo_setting_id = $voucher_data['promo_setting_id'];
}
// print_r($final_cart_items);exit;
$promo = $this->promo_service->checkPromoLogic($promo_setting_id, $final_cart_items, number_format($subtotal_without_options, 2), $cart['customer_id'], $outlet_id, $order_type, $delivery_fee);
if (isset($promo['status']) && $promo['status'] == 400) {
//remove from cart
$result = $this->resetVoucher($cart_id);
// print_r($promo);
// exit;
return $promo;
}
if (isset($promo['status']) && $promo['status'] == 200) {
// print_r($promo);exit;
if ($promo['delivery_fee'] > 0) {
$promo_discount_amount += $promo['delivery_fee'];
}
if ($promo['promo_discount_total'] > 0) {
$promo_discount_amount += $promo['promo_discount_total'];
}
$free_item_list = $promo['free_item_list'];
}
}
$tax_detail = [];
$grand_total = $subtotal + $packaging_charge - $total_discount - $promo_discount_amount;
if($grand_total < 0){
$grand_total = 0;
}
$tax_array = $this->setting_tax->where("FIND_IN_SET($outlet_id, outlet_id)")->where("FIND_IN_SET('" . $order_type . "', order_type)")->findAll();
foreach ($tax_array as $tax) {
$add_tax = $grand_total * ($tax['tax_rate'] / 100);
$total_tax += $add_tax;
$tax_detail[] = [
'tax_type' => $tax['tax_type'],
'tax_rate' => $tax['tax_rate'],
'tax_amount' => number_format_no_round($add_tax)
];
}
//add tax
// echo $total_tax;exit;
$grand_total += $total_tax;
$grand_total = number_format_no_round($grand_total);
// echo($grand_total);exit;
$rounding_amount = calculateRoundingAmount($grand_total);
// echo $rounding_amount;exit;
$grand_total = $grand_total + $rounding_amount;
// echo $grand_total;exit;
//calculate rounding amount
//call delivery service api if delivery
// print_r($final_cart_items);exit;
// Update cart totals in database
$this->carts->update($cart_id, [
'item_count' => count($cart_items),
'subtotal_amount' => number_format($subtotal, 2),
'discount_amount' => number_format($total_discount, 2),
'tax_amount' => number_format($total_tax, 2),
'grand_total' => number_format($grand_total, 2),
'rounding_amount' => number_format($rounding_amount, 2),
'promo_discount_amount' => $promo_or_voucher == 'promo' ? number_format($promo_discount_amount, 2) : 0.00,
'voucher_discount_amount' => $promo_or_voucher == 'voucher' ? number_format($promo_discount_amount, 2) : 0.00,
'delivery_fee' => number_format($delivery_fee, 2),
'selected_date' => $selected_date,
'selected_time' => $selected_time,
'latitude' => $latitude,
'longitude' => $longitude,
'order_type' => $order_type,
'address' => $address,
'lalamove_quot_id' => $delivery_service === 'Lalamove' ? $quotation_id : null,
'grab_quot_id' => $delivery_service === 'Grab' ? $quotation_id : null
]);
// print_r($final_cart_items);exit;
//check pwp
$pwp_menu = [];
$pwp_discount = $this->checkItemPwpDiscount($cart_id);
// print_r($final_cart_items);exit;
if($pwp_discount['has_discount']){
$pwp_items = $pwp_discount['pwp_item'];
foreach($pwp_items as $pwp_item){
$pwp_menu_item = $this->menu_items->where('id', $pwp_item)->first();
if(isset($pwp_menu_item['pwp_price'])){
if($pwp_menu_item['pwp_price'] > 0){
$pwp_menu[] = $pwp_menu_item;
}
}
}
}
// print_r($pwp_menu);exit;
// Return the complete structure
return [
'status' => 200,
'message' => 'Cart retrieved successfully.',
'data' => [
'id' => $cart['id'],
'customer_id' => $cart['customer_id'],
'outlet_id' => $cart['outlet_id'],
'status' => $cart['status'],
'order_summary' => [
'item_count' => count($final_cart_items),
'subtotal_amount' => number_format($subtotal, 2),
'discount_amount' => number_format($total_discount, 2),
'tax_amount' => number_format($total_tax, 2),
'delivery_fee' => number_format($delivery_fee, 2),
'packaging_charge' => number_format($packaging_charge, 2),
// 'packaging_charge' => "123",
'grand_total' => number_format($grand_total, 2),
'store_discount' => $array_discount,
'store_discount_amount' => number_format($total_discount, 2),
'grand_total_without_rounding' => number_format($grand_total - $rounding_amount, 2),
'rounding_amount' => number_format($rounding_amount, 2),
'promo_code_id' => $promo_code_id ?? 0,
'promo_discount_amount' => $promo_or_voucher == 'promo' ? number_format($promo_discount_amount, 2) : 0.00,
'promo_code' => $promo_code,
'customer_voucher_list_id' => $cart['customer_voucher_list_id'],
'voucher_discount_amount' => $promo_or_voucher == 'voucher' ? number_format($promo_discount_amount, 2) : 0.00,
'voucher_code' => $voucher_code,
'selected_date' => $selected_date,
'selected_time' => $selected_time,
'latitude' => $latitude,
'longitude' => $longitude,
'order_type' => $order_type,
'address' => $address
],
'tax_detail' => $tax_detail,
'invalid_items' => $invalid_items,
'items' => $final_cart_items,
'free_item_list' => $free_item_list,
'pwp_menu' => $pwp_menu
]
];
}
public function resetVoucher($cart_id)
{
// Reset promo and voucher fields
$this->carts->update($cart_id, [
'promo_code_id' => 0,
'promo_discount_amount' => 0,
'customer_voucher_list_id' => 0,
'voucher_discount_amount' => 0
]);
// Get free items from cart
$freeItems = $this->cart_items
->select('id')
->where('cart_id', $cart_id)
->where('is_free_item', 1)
->findAll();
if (!empty($freeItems)) {
// Collect all item IDs
$itemIds = array_column($freeItems, 'id');
// Delete all related item options in one query
$this->cart_item_options->whereIn('cart_item_id', $itemIds)->delete();
// Delete all free items in one query
$this->cart_items->whereIn('id', $itemIds)->delete();
}
}
/**
* Calculate distance from outlet to customer using Google Distance Matrix API
* @param int $outlet_id
* @param float $latitude
* @param float $longitude
* @return float Distance in kilometers
*/
public function calculateDistance($outlet_id, $latitude, $longitude)
{
try {
// Validate input parameters
if (empty($outlet_id) || !is_numeric($outlet_id)) {
log_message('error', 'CalculateService: Invalid outlet ID provided: ' . $outlet_id);
return 0;
}
if (!is_numeric($latitude) || !is_numeric($longitude)) {
log_message('error', 'CalculateService: Invalid coordinates provided - lat: ' . $latitude . ', lng: ' . $longitude);
return 0;
}
// Validate coordinate ranges
if ($latitude < -90 || $latitude > 90 || $longitude < -180 || $longitude > 180) {
log_message('error', 'CalculateService: Coordinates out of valid range - lat: ' . $latitude . ', lng: ' . $longitude);
return 0;
}
// Get outlet coordinates
$outlet = $this->outlet->find($outlet_id);
if (!$outlet || !isset($outlet['latitude']) || !isset($outlet['longitude'])) {
log_message('error', 'CalculateService: Outlet not found or missing coordinates for outlet ID: ' . $outlet_id);
return 0;
}
$outlet_lat = $outlet['latitude'];
$outlet_lng = $outlet['longitude'];
// Check if we have valid outlet coordinates
if (!is_numeric($outlet_lat) || !is_numeric($outlet_lng)) {
log_message('error', 'CalculateService: Invalid outlet coordinates - lat: ' . $outlet_lat . ', lng: ' . $outlet_lng);
return 0;
}
// Check if Google API key is configured
if (empty(GOOGLE_DISTANCE_MATRIX_API_KEY) || GOOGLE_DISTANCE_MATRIX_API_KEY === 'YOUR_GOOGLE_API_KEY_HERE') {
log_message('error', 'CalculateService: Google Distance Matrix API key not configured');
return 0;
}
// Google Distance Matrix API endpoint
$api_url = 'https://maps.googleapis.com/maps/api/distancematrix/json';
// Build the request URL
$params = [
'origins' => $outlet_lat . ',' . $outlet_lng,
'destinations' => $latitude . ',' . $longitude,
'mode' => 'driving',
'units' => 'metric',
'key' => GOOGLE_DISTANCE_MATRIX_API_KEY
];
$url = $api_url . '?' . http_build_query($params);
log_message('info', 'CalculateService: Requesting distance from Google API - Outlet: ' . $outlet_id . ', Customer: ' . $latitude . ',' . $longitude);
// Make the API request using the helper function
$response = send_api_request('GET', $url, [], null, true);
if ($response === false) {
log_message('error', 'CalculateService: Failed to get response from Google Distance Matrix API');
return 0;
}
$data = $response;
// Check for API errors
if ($data['status'] !== 'OK') {
$error_message = $data['error_message'] ?? $data['status'] ?? 'Unknown error';
log_message('error', 'CalculateService: Google Distance Matrix API error: ' . $error_message);
// Log additional error details if available
if (isset($data['error_message'])) {
log_message('error', 'CalculateService: Google API Error Message: ' . $data['error_message']);
}
if (isset($data['status'])) {
log_message('error', 'CalculateService: Google API Status: ' . $data['status']);
}
return 0;
}
// Extract distance from response
if (isset($data['rows'][0]['elements'][0]['distance']['value'])) {
$distance_meters = $data['rows'][0]['elements'][0]['distance']['value'];
$distance_km = $distance_meters / 1000;
log_message('info', 'CalculateService: Distance calculated successfully - Outlet: ' . $outlet_id . ', Distance: ' . $distance_km . ' km');
return round($distance_km, 2);
} else {
// Check if the route is not found
if (isset($data['rows'][0]['elements'][0]['status']) && $data['rows'][0]['elements'][0]['status'] === 'NOT_FOUND') {
log_message('warning', 'CalculateService: Route not found between outlet and customer location');
} else {
log_message('warning', 'CalculateService: No distance data in Google API response');
}
return 0;
}
} catch (Exception $e) {
log_message('error', 'CalculateService: Exception in calculateDistance: ' . $e->getMessage());
log_message('error', 'CalculateService: Stack trace: ' . $e->getTraceAsString());
return 0;
}
}
/**
* Calculate delivery fee based on distance
* @param float $distance Distance in kilometers
* @return float Delivery fee in RM
*/
public function calculateDeliveryFee($distance)
{
// Validate input
if (!is_numeric($distance) || $distance < 0) {
log_message('warning', 'CalculateService: Invalid distance provided for fee calculation: ' . $distance);
return 0;
}
if ($distance == 0) {
return 0;
}
// Delivery fee structure based on distance
$fee = 0;
//get setting delivery fee
$setting_delivery_fee = $this->setting_delivery_fee->findAll();
foreach($setting_delivery_fee as $setting){
if($distance >= $setting['start_km'] && $distance <= $setting['end_km']){
$fee = $setting['price_per_km'];
}
}
// Ensure fee is a valid number and round to 2 decimal places
$fee = round($fee, 2);
log_message('info', 'CalculateService: Delivery fee calculated - Distance: ' . $distance . ' km, Fee: RM ' . $fee);
return $fee;
}
}