From d298649398766a3fcf77308eb16eff75a2272cde Mon Sep 17 00:00:00 2001 From: Bayu Lukman Yusuf Date: Tue, 27 Jan 2026 21:41:25 +0700 Subject: [PATCH] checkout page, fill address, choose shipping, calculation --- app/Http/Controllers/CheckoutController.php | 159 ++++ .../Member/Transaction/CancelRequest.php | 31 + .../Member/Transaction/CloseRequest.php | 31 + .../Member/Transaction/DeliverRequest.php | 30 + .../Member/Transaction/DetailRequest.php | 31 + .../Member/Transaction/ProcessRequest.php | 30 + .../Member/Transaction/TransactionRequest.php | 34 + app/Models/LuckyWheel.php | 23 + app/Models/LuckyWheelGet.php | 21 + app/Models/LuckyWheelPrize.php | 28 + app/Models/LuckyWheelTicket.php | 38 + app/Models/PosInvoice.php | 82 ++ app/Models/PosInvoiceDetail.php | 35 + app/Models/PosInvoicePayment.php | 18 + app/Models/PosInvoiceVoucher.php | 29 + app/Models/Sales.php | 70 ++ app/Models/SerialNumber.php | 25 + app/Models/SerialNumberDetail.php | 27 + app/Models/SerialNumberLog.php | 14 + app/Models/Survey.php | 24 + app/Models/SurveyFeedback.php | 29 + app/Models/SurveyFeedbackDetail.php | 13 + app/Models/SurveyQuestion.php | 16 + app/Models/XenditLink.php | 34 + app/Notifications/FcmChannel.php | 57 ++ .../Member/Transaction/NewOrder.php | 83 ++ .../Member/Transaction/OrderCanceled.php | 84 ++ .../Member/Transaction/OrderDelivered.php | 83 ++ .../Member/Transaction/OrderOnDelivery.php | 83 ++ .../Member/Transaction/OrderPaid.php | 83 ++ .../Member/Transaction/OrderProcessed.php | 83 ++ .../Member/Transaction/OrderWaitPayment.php | 95 ++ app/Repositories/Crm/SurveyRepository.php | 219 +++++ .../Member/Cart/MemberCartRepository.php | 12 + .../Member/ShippingRepository.php | 255 +++++ .../Member/Transaction/CheckoutController.php | 26 + .../Transaction/TransactionRepository.php | 872 ++++++++++++++++++ app/Repositories/Pos/InvoiceRepository.php | 842 +++++++++++++++++ app/ThirdParty/Biteship/Biteship.php | 40 + app/ThirdParty/Biteship/Order.php | 116 +++ app/ThirdParty/Biteship/Rate.php | 81 ++ app/ThirdParty/Biteship/Tracking.php | 54 ++ lang/en/cart_summary.php | 15 + lang/en/checkout.php | 47 + lang/id/cart_summary.php | 15 + lang/id/checkout.php | 36 + resources/views/checkout/v1-cart.blade.php | 2 +- .../checkout/v1-delivery-1-shipping.blade.php | 460 +++++++++ .../views/checkout/v1-delivery-1.blade.php | 666 ++++++++++--- .../checkout/order-summary.blade.php | 67 ++ routes/web.php | 13 + 51 files changed, 5212 insertions(+), 149 deletions(-) create mode 100644 app/Http/Controllers/CheckoutController.php create mode 100644 app/Http/Requests/Member/Transaction/CancelRequest.php create mode 100644 app/Http/Requests/Member/Transaction/CloseRequest.php create mode 100644 app/Http/Requests/Member/Transaction/DeliverRequest.php create mode 100644 app/Http/Requests/Member/Transaction/DetailRequest.php create mode 100644 app/Http/Requests/Member/Transaction/ProcessRequest.php create mode 100644 app/Http/Requests/Member/Transaction/TransactionRequest.php create mode 100644 app/Models/LuckyWheel.php create mode 100644 app/Models/LuckyWheelGet.php create mode 100644 app/Models/LuckyWheelPrize.php create mode 100644 app/Models/LuckyWheelTicket.php create mode 100644 app/Models/PosInvoice.php create mode 100644 app/Models/PosInvoiceDetail.php create mode 100644 app/Models/PosInvoicePayment.php create mode 100644 app/Models/PosInvoiceVoucher.php create mode 100644 app/Models/Sales.php create mode 100644 app/Models/SerialNumber.php create mode 100644 app/Models/SerialNumberDetail.php create mode 100644 app/Models/SerialNumberLog.php create mode 100644 app/Models/Survey.php create mode 100644 app/Models/SurveyFeedback.php create mode 100644 app/Models/SurveyFeedbackDetail.php create mode 100644 app/Models/SurveyQuestion.php create mode 100644 app/Models/XenditLink.php create mode 100644 app/Notifications/FcmChannel.php create mode 100644 app/Notifications/Member/Transaction/NewOrder.php create mode 100644 app/Notifications/Member/Transaction/OrderCanceled.php create mode 100644 app/Notifications/Member/Transaction/OrderDelivered.php create mode 100644 app/Notifications/Member/Transaction/OrderOnDelivery.php create mode 100644 app/Notifications/Member/Transaction/OrderPaid.php create mode 100644 app/Notifications/Member/Transaction/OrderProcessed.php create mode 100644 app/Notifications/Member/Transaction/OrderWaitPayment.php create mode 100644 app/Repositories/Crm/SurveyRepository.php create mode 100644 app/Repositories/Member/ShippingRepository.php create mode 100644 app/Repositories/Member/Transaction/CheckoutController.php create mode 100644 app/Repositories/Member/Transaction/TransactionRepository.php create mode 100644 app/Repositories/Pos/InvoiceRepository.php create mode 100644 app/ThirdParty/Biteship/Biteship.php create mode 100644 app/ThirdParty/Biteship/Order.php create mode 100644 app/ThirdParty/Biteship/Rate.php create mode 100644 app/ThirdParty/Biteship/Tracking.php create mode 100644 lang/en/cart_summary.php create mode 100644 lang/en/checkout.php create mode 100644 lang/id/cart_summary.php create mode 100644 lang/id/checkout.php create mode 100644 resources/views/checkout/v1-delivery-1-shipping.blade.php create mode 100644 resources/views/components/checkout/order-summary.blade.php diff --git a/app/Http/Controllers/CheckoutController.php b/app/Http/Controllers/CheckoutController.php new file mode 100644 index 0000000..ca79eb8 --- /dev/null +++ b/app/Http/Controllers/CheckoutController.php @@ -0,0 +1,159 @@ +merge(['location_id' => $request->input('location_id', session('location_id', 22))]); + + $store = Location::find($request->input('location_id')); + + $subtotal = $memberCartRepository->getSubtotal($request->input('location_id')); + + $address_list = Address::where('user_id', $request->user()->id)->orderBy('is_primary','desc')->get(); + + + $total = $subtotal; + return view('checkout.v1-delivery-1', [ + 'carts' => $request->user()->carts, + 'subtotal' => $subtotal, + 'total' => $total, + 'store' => $store, + 'address_list' => $address_list, + ]); + } + + public function indexProcess(Request $request) + { + $delivery_method = $request->input('delivery_method') ?? 'shipping'; + $address_id = $request->input('address_id'); + + if ($address_id == null) { + $address_list = Address::where('user_id', $request->user()->id)->orderBy('is_primary','desc')->get(); + $address_id = $address_list->first()->id; + } + + if ($delivery_method == null || $address_id == null) { + return redirect()->back()->with('error', 'Delivery method or address is required'); + } + + + if ($delivery_method == 'shipping') { + session(['checkout_delivery_method' => $delivery_method]); + session(['checkout_address_id' => $address_id]); + return redirect()->route('checkout.shipping'); + } + + if ($delivery_method == 'pickup') { + session(['checkout_delivery_method' => $delivery_method]); + session(['checkout_address_id' => null]); + return redirect()->route('checkout.payment'); + } + + return redirect()->back()->with('error', 'Delivery method is not valid'); + } + + + public function chooseShipping(Request $request, MemberCartRepository $memberCartRepository) + { + try { + $delivery_method = $request->input('delivery_method'); + $address_id = $request->input('address_id'); + + $subtotal = $memberCartRepository->getSubtotal($request->input('location_id')); + $total = $subtotal; + + + + $shipping_list = [ + [ + 'courier' => 'JNE', + 'service' => 'REG', + 'title' => 'JNE Regular', + 'cost' => 10000, + ], + [ + 'courier' => 'JNE', + 'service' => 'YES', + 'title' => 'JNE YES (Same Day)', + 'cost' => 25000, + ], + [ + 'courier' => 'J&T', + 'service' => 'EZ', + 'title' => 'J&T Express', + 'cost' => 12000, + ], + [ + 'courier' => 'SiCepat', + 'service' => 'REG', + 'title' => 'SiCepat Regular', + 'cost' => 11000, + ], + [ + 'courier' => 'Gojek', + 'service' => 'Same Day', + 'title' => 'Gojek Same Day', + 'cost' => 20000, + ], + [ + 'courier' => 'Grab', + 'service' => 'Instant', + 'title' => 'Grab Instant', + 'cost' => 22000, + ], + ]; + + return view('checkout.v1-delivery-1-shipping', [ + 'carts' => $request->user()->carts, + 'subtotal' => $subtotal, + 'total' => $total, + 'delivery_method' => $delivery_method, + 'address_id' => $address_id, + 'address' => Address::find($address_id), + 'shipping_list' => $shipping_list, + ]); + } catch (\Exception $e) { + return redirect()->route('checkout.delivery')->with('error', 'Invalid checkout data'); + } + } + + public function chooseShippingProcess(Request $request) + { + $shipping_option = $request->input('shipping_option'); + + // Parse shipping option (format: courier|service|cost) + $shipping_data = explode('|', $shipping_option); + $courier = $shipping_data[0] ?? ''; + $service = $shipping_data[1] ?? ''; + $cost = $shipping_data[2] ?? 0; + + session(['checkout_courier' => $courier]); + session(['checkout_service' => $service]); + session(['checkout_shipping_cost' => $cost]); + + return redirect()->route('checkout.payment'); + } + + public function choosePayment(Request $request) + { + try { + + // proses checkout + + + // proses payment + + + } catch (\Exception $e) { + return redirect()->route('checkout.delivery')->with('error', 'Invalid checkout data'); + } + } +} diff --git a/app/Http/Requests/Member/Transaction/CancelRequest.php b/app/Http/Requests/Member/Transaction/CancelRequest.php new file mode 100644 index 0000000..4e56fcf --- /dev/null +++ b/app/Http/Requests/Member/Transaction/CancelRequest.php @@ -0,0 +1,31 @@ +transaction; + $user = auth()->user(); + $isAdmin = auth()->user()->role->permissions->contains(function($value){ + return $value->code == "transaction.online"; + }); + $isOwner = (@$transaction->customer->user->id == @$user->id) && ($transaction->status == 'WAIT_PAYMENT' || $transaction->status == 'WAIT_PROCESS'); + return $isAdmin || $isOwner; + } + + public function rules() + { + return [ + 'note' => 'nullable|string', + ]; + } +} diff --git a/app/Http/Requests/Member/Transaction/CloseRequest.php b/app/Http/Requests/Member/Transaction/CloseRequest.php new file mode 100644 index 0000000..8e6b7bb --- /dev/null +++ b/app/Http/Requests/Member/Transaction/CloseRequest.php @@ -0,0 +1,31 @@ +transaction; + $user = auth()->user(); + $isAdmin = auth()->user()->role->permissions->contains(function($value){ + return $value->code == "transaction.online"; + }); + $isOwner = (@$transaction->customer->user->id == @$user->id) && ($transaction->status == 'WAIT_PAYMENT' || $transaction->status == 'WAIT_PROCESS'); + return $isAdmin || $isOwner; + } + + public function rules() + { + return [ + 'note' => 'nullable|string', + ]; + } +} diff --git a/app/Http/Requests/Member/Transaction/DeliverRequest.php b/app/Http/Requests/Member/Transaction/DeliverRequest.php new file mode 100644 index 0000000..c27bf70 --- /dev/null +++ b/app/Http/Requests/Member/Transaction/DeliverRequest.php @@ -0,0 +1,30 @@ +transaction; + $user = auth()->user(); + $isAdmin = auth()->user()->role->permissions->contains(function($value){ + return $value->code == "transaction.online"; + }); + return $isAdmin; + } + + public function rules() + { + return [ + 'note' => 'nullable|string', + ]; + } +} diff --git a/app/Http/Requests/Member/Transaction/DetailRequest.php b/app/Http/Requests/Member/Transaction/DetailRequest.php new file mode 100644 index 0000000..c38ef38 --- /dev/null +++ b/app/Http/Requests/Member/Transaction/DetailRequest.php @@ -0,0 +1,31 @@ +transaction; + $user = auth()->user(); + $isAdmin = auth()->user()->role->permissions->contains(function($value){ + return $value->code == "transaction.online"; + }); + $isOwner = (@$transaction->customer->user->id == @$user->id); + return $isAdmin || $isOwner; + } + + public function rules() + { + return [ + 'note' => 'nullable|string', + ]; + } +} diff --git a/app/Http/Requests/Member/Transaction/ProcessRequest.php b/app/Http/Requests/Member/Transaction/ProcessRequest.php new file mode 100644 index 0000000..5788c7d --- /dev/null +++ b/app/Http/Requests/Member/Transaction/ProcessRequest.php @@ -0,0 +1,30 @@ +transaction; + $user = auth()->user(); + $isAdmin = auth()->user()->role->permissions->contains(function($value){ + return $value->code == "transaction.online"; + }); + return $isAdmin; + } + + public function rules() + { + return [ + 'note' => 'nullable|string', + ]; + } +} diff --git a/app/Http/Requests/Member/Transaction/TransactionRequest.php b/app/Http/Requests/Member/Transaction/TransactionRequest.php new file mode 100644 index 0000000..ab26f98 --- /dev/null +++ b/app/Http/Requests/Member/Transaction/TransactionRequest.php @@ -0,0 +1,34 @@ + 'required|integer|exists:address,id', + 'location_id' => 'required|integer', + 'courier_company' => 'required|string', + 'courier_type' => 'required|string', + 'vouchers' => 'nullable|array', + 'vouchers.*' => 'required|integer', + 'items' => 'nullable|array', + 'items.*.item_reference_id' => 'required|integer', + 'items.*.qty' => 'required|integer', + 'use_customer_points' => 'nullable|integer', + ]; + } +} diff --git a/app/Models/LuckyWheel.php b/app/Models/LuckyWheel.php new file mode 100644 index 0000000..a5c32f9 --- /dev/null +++ b/app/Models/LuckyWheel.php @@ -0,0 +1,23 @@ +hasMany(LuckyWheelPrize::class); + } + + public function gets(){ + return $this->hasMany(LuckyWheelGet::class); + } + + public function tickets(){ + return $this->hasMany(LuckyWheelTicket::class); + } +} diff --git a/app/Models/LuckyWheelGet.php b/app/Models/LuckyWheelGet.php new file mode 100644 index 0000000..4c64e2b --- /dev/null +++ b/app/Models/LuckyWheelGet.php @@ -0,0 +1,21 @@ +belongsTo(LuckyWheelPrize::class,'prize_id'); + } + + public function voucher(){ + return $this->belongsTo(Voucher::class,'voucher_id'); + } +} \ No newline at end of file diff --git a/app/Models/LuckyWheelPrize.php b/app/Models/LuckyWheelPrize.php new file mode 100644 index 0000000..d4c1269 --- /dev/null +++ b/app/Models/LuckyWheelPrize.php @@ -0,0 +1,28 @@ +belongsTo(Voucher::class); + } + + public function voucherEvent(){ + return $this->belongsTo(VoucherEvent::class); + } + + public function gets(){ + return $this->hasMany(LuckyWheelGet::class,"prize_id") + ->whereDate("created_at", Carbon::now()); + } + +} diff --git a/app/Models/LuckyWheelTicket.php b/app/Models/LuckyWheelTicket.php new file mode 100644 index 0000000..a70dc51 --- /dev/null +++ b/app/Models/LuckyWheelTicket.php @@ -0,0 +1,38 @@ +belongsTo(LuckyWheel::class); + } + + public function customer() + { + return $this->belongsTo(Customer::class); + } + + public function prize() + { + return $this->hasOne(LuckyWheelPrize::class,"ticket_id")->with('voucher'); + } + + public function gets() + { + return $this->hasMany(LuckyWheelGet::class,"ticket_id")->with("prize","voucher")->orderBy("created_at","desc"); + } + + public function get() + { + return $this->hasOne(LuckyWheelGet::class,"ticket_id")->whereNotNull("redeem_at")->with("voucher","prize"); + } +} \ No newline at end of file diff --git a/app/Models/PosInvoice.php b/app/Models/PosInvoice.php new file mode 100644 index 0000000..5049db3 --- /dev/null +++ b/app/Models/PosInvoice.php @@ -0,0 +1,82 @@ +hasOne(LuckyWheelTicket::class, 'invoice_id')->with("prize","gets"); + } + + public function customer(){ + return $this->belongsTo(Customer::class); + } + + public function sales(){ + return $this->belongsTo(Sales::class); + } + + public function sales2(){ + return $this->belongsTo(Sales::class, 'sales2_id', 'id'); + } + + public function location(){ + return $this->belongsTo(Location::class); + } + + public function user(){ + return $this->belongsTo(User::class); + } + + public function details(){ + return $this->hasMany(PosInvoiceDetail::class,"invoice_id") + ->orderBy("line_no","asc"); + } + + public function payments(){ + return $this->hasMany(PosInvoicePayment::class,"invoice_id"); + } + + public function canceledBy(){ + return $this->belongsTo(User::class,"canceled_by"); + } + + public function vouchers(){ + return $this->morphMany(Voucher::class,'reference_used'); + } + + public function discountVouchers(){ + return $this->hasMany(PosInvoiceVoucher::class, 'pos_invoice_id'); + } + + public function getPoint() { + $total = CustomerPoint::where('reference_type', 'App\Models\PosInvoice') + ->where('reference_id', $this->id) + ->sum('point'); + + return $total; + } + + public function feedback(){ + return $this->hasOne(SurveyFeedback::class,"invoice_id"); + } +} diff --git a/app/Models/PosInvoiceDetail.php b/app/Models/PosInvoiceDetail.php new file mode 100644 index 0000000..783e304 --- /dev/null +++ b/app/Models/PosInvoiceDetail.php @@ -0,0 +1,35 @@ +belongsTo(ItemReference::class); + } + + public function item() + { + return $this->belongsTo(Items::class); + } + + public function itemVariant() + { + return $this->belongsTo(ItemVariant::class); + } + + public function invoice() + { + return $this->belongsTo(PosInvoice::class, 'invoice_id'); + } +} diff --git a/app/Models/PosInvoicePayment.php b/app/Models/PosInvoicePayment.php new file mode 100644 index 0000000..4c14c0e --- /dev/null +++ b/app/Models/PosInvoicePayment.php @@ -0,0 +1,18 @@ +belongsTo(Bank::class, 'bank_id', 'id'); + } +} diff --git a/app/Models/PosInvoiceVoucher.php b/app/Models/PosInvoiceVoucher.php new file mode 100644 index 0000000..0f480c8 --- /dev/null +++ b/app/Models/PosInvoiceVoucher.php @@ -0,0 +1,29 @@ +belongsTo(PosInvoice::class, 'pos_invoice_id'); + } + + public function voucher() + { + return $this->belongsTo(Voucher::class, 'voucher_id'); + } +} diff --git a/app/Models/Sales.php b/app/Models/Sales.php new file mode 100644 index 0000000..70ce2a8 --- /dev/null +++ b/app/Models/Sales.php @@ -0,0 +1,70 @@ + [ + 'source' => 'number' + ] + ]; + } + + public function scopeFilter(Builder $query, array $filters) + { + $query->when($filters['search'] ?? false, function ($query, $search) { + return $query + ->where('name', 'iLIKE', '%' . $search . '%') + ->orWhere('phone', 'LIKE', '%' . $search . '%'); + }); + } + + public function locations() + { + return $this->belongsTo(Location::class, 'location_id', 'id'); + } + + public function customerGroup() + { + return $this->belongsTo(CustomerGroup::class, 'customer_group_id', 'id'); + } +} diff --git a/app/Models/SerialNumber.php b/app/Models/SerialNumber.php new file mode 100644 index 0000000..49498cd --- /dev/null +++ b/app/Models/SerialNumber.php @@ -0,0 +1,25 @@ +hasMany(SerialNumberDetail::class, 'sn_batch_id', 'id'); + } + + public function user() { + return $this->belongsTo(User::class); + } +} diff --git a/app/Models/SerialNumberDetail.php b/app/Models/SerialNumberDetail.php new file mode 100644 index 0000000..ad7bd30 --- /dev/null +++ b/app/Models/SerialNumberDetail.php @@ -0,0 +1,27 @@ +belongsTo(SerialNumber::class,"sn_batch_id"); + } + + public function invoice(){ + return $this->belongsTo(PosInvoice::class,"invoice_id"); + } + + public function activatedBy(){ + return $this->belongsTo(User::class,"activated_by"); + } +} diff --git a/app/Models/SerialNumberLog.php b/app/Models/SerialNumberLog.php new file mode 100644 index 0000000..5e9fecb --- /dev/null +++ b/app/Models/SerialNumberLog.php @@ -0,0 +1,14 @@ +hasMany(SurveyQuestion::class); + } + + public function voucherEvent() { + return $this->belongsTo(VoucherEvent::class); + } +} diff --git a/app/Models/SurveyFeedback.php b/app/Models/SurveyFeedback.php new file mode 100644 index 0000000..10a514c --- /dev/null +++ b/app/Models/SurveyFeedback.php @@ -0,0 +1,29 @@ +hasMany(SurveyFeedbackDetail::class); + } + + public function survey() { + return $this->belongsTo(Survey::class); + } + + public function customer() { + return $this->belongsTo(Customer::class); + } + + public function invoice() { + return $this->belongsTo(PosInvoice::class); + } +} diff --git a/app/Models/SurveyFeedbackDetail.php b/app/Models/SurveyFeedbackDetail.php new file mode 100644 index 0000000..198dcf3 --- /dev/null +++ b/app/Models/SurveyFeedbackDetail.php @@ -0,0 +1,13 @@ +morphOne(TransactionPayment::class,"method"); + } +} diff --git a/app/Notifications/FcmChannel.php b/app/Notifications/FcmChannel.php new file mode 100644 index 0000000..042c2db --- /dev/null +++ b/app/Notifications/FcmChannel.php @@ -0,0 +1,57 @@ +toFcm($notifiable); + + // if ($notifiable->fcm_token != null){ + // $this->sendNotification($payload, $notifiable->fcm_token, $notifiable); + // } + + // foreach($notifiable->devices as $device){ + // if ($device->fcm_token == $notifiable->fcm_token) + // continue; + + // $this->sendNotification($payload, $device->fcm_token, $device); + // sleep(0.5); + // } + } + + private function sendNotification($payload, $fcm_token, $device){ + + // if (!$fcm_token) + // return; + + // try{ + // $payload["token"] = $fcm_token; + // $messaging = app('firebase.messaging'); + // $message = CloudMessage::fromArray($payload); + // $result = $messaging->send($message); + // }catch(\Kreait\Firebase\Exception\Messaging\NotFound $e){ + // if (get_class($device) == UserDevice::class){ + // $device->fcm_token = null; + // $device->save(); + // }else if (get_class($device) == User::class){ + // $device->fcm_token = null; + // $device->save(); + // } + // }catch(\Kreait\Firebase\Exception\Messaging\InvalidMessage $e){ + // \Log::info([$fcm_token, $e->getMessage()]); + // }catch(\Exception $e){ + // report($e); + // } + + } +} diff --git a/app/Notifications/Member/Transaction/NewOrder.php b/app/Notifications/Member/Transaction/NewOrder.php new file mode 100644 index 0000000..b8ec578 --- /dev/null +++ b/app/Notifications/Member/Transaction/NewOrder.php @@ -0,0 +1,83 @@ +transaction = $transaction; + } + + /** + * Get the notification's delivery channels. + * + * @param mixed $notifiable + * @return array + */ + public function via($notifiable) + { + return ['database',FcmChannel::class]; + } + + /** + * Get the mail representation of the notification. + * + * @param mixed $notifiable + * @return \Illuminate\Notifications\Messages\MailMessage + */ + public function toMail($notifiable) + { + return (new MailMessage) + ->line('The introduction to the notification.') + ->action('Notification Action', url('/')) + ->line('Thank you for using our application!'); + } + + /** + * Get the array representation of the notification. + * + * @param mixed $notifiable + * @return array + */ + public function toArray($notifiable) + { + $transaction = $this->transaction; + return [ + "title" => "Ada Pesanan baru masuk!", + "body" => "Mohon segera proses pesanan nomor $transaction->number", + "type" => "Info", + "data" => $this->transaction, + "model" => get_class($this->transaction) + ]; + } + + public function toFcm($notifiable) + { + $payload = $this->toArray($notifiable); + + return [ + "notification" => [ + "title" => $payload["title"], + "body" => $payload["body"], + ] + ]; + } +} diff --git a/app/Notifications/Member/Transaction/OrderCanceled.php b/app/Notifications/Member/Transaction/OrderCanceled.php new file mode 100644 index 0000000..54fa21e --- /dev/null +++ b/app/Notifications/Member/Transaction/OrderCanceled.php @@ -0,0 +1,84 @@ +transaction = $transaction; + } + + /** + * Get the notification's delivery channels. + * + * @param mixed $notifiable + * @return array + */ + public function via($notifiable) + { + return ['database',FcmChannel::class]; + } + + /** + * Get the mail representation of the notification. + * + * @param mixed $notifiable + * @return \Illuminate\Notifications\Messages\MailMessage + */ + public function toMail($notifiable) + { + return (new MailMessage) + ->line('The introduction to the notification.') + ->action('Notification Action', url('/')) + ->line('Thank you for using our application!'); + } + + /** + * Get the array representation of the notification. + * + * @param mixed $notifiable + * @return array + */ + public function toArray($notifiable) + { + $transaction = $this->transaction; + $status = $transaction->statuses()->where("status","CANCELED")->first(); + return [ + "title" => "Pesanan telah dibatalkan!", + "body" => "Pesanan $transaction->number telah dibatalkan $status->note", + "type" => "Info", + "data" => $this->transaction, + "model" => get_class($this->transaction) + ]; + } + + public function toFcm($notifiable) + { + $payload = $this->toArray($notifiable); + + return [ + "notification" => [ + "title" => $payload["title"], + "body" => $payload["body"], + ] + ]; + } +} diff --git a/app/Notifications/Member/Transaction/OrderDelivered.php b/app/Notifications/Member/Transaction/OrderDelivered.php new file mode 100644 index 0000000..3330934 --- /dev/null +++ b/app/Notifications/Member/Transaction/OrderDelivered.php @@ -0,0 +1,83 @@ +transaction = $transaction; + } + + /** + * Get the notification's delivery channels. + * + * @param mixed $notifiable + * @return array + */ + public function via($notifiable) + { + return ['database',FcmChannel::class]; + } + + /** + * Get the mail representation of the notification. + * + * @param mixed $notifiable + * @return \Illuminate\Notifications\Messages\MailMessage + */ + public function toMail($notifiable) + { + return (new MailMessage) + ->line('The introduction to the notification.') + ->action('Notification Action', url('/')) + ->line('Thank you for using our application!'); + } + + /** + * Get the array representation of the notification. + * + * @param mixed $notifiable + * @return array + */ + public function toArray($notifiable) + { + $transaction = $this->transaction; + return [ + "title" => "Pesanan sudah tiba!", + "body" => "Pesanan $transaction->number sudah tiba di alamat tujuan", + "type" => "Info", + "data" => $this->transaction, + "model" => get_class($this->transaction) + ]; + } + + public function toFcm($notifiable) + { + $payload = $this->toArray($notifiable); + + return [ + "notification" => [ + "title" => $payload["title"], + "body" => $payload["body"], + ] + ]; + } +} diff --git a/app/Notifications/Member/Transaction/OrderOnDelivery.php b/app/Notifications/Member/Transaction/OrderOnDelivery.php new file mode 100644 index 0000000..e4cc50a --- /dev/null +++ b/app/Notifications/Member/Transaction/OrderOnDelivery.php @@ -0,0 +1,83 @@ +transaction = $transaction; + } + + /** + * Get the notification's delivery channels. + * + * @param mixed $notifiable + * @return array + */ + public function via($notifiable) + { + return ['database',FcmChannel::class]; + } + + /** + * Get the mail representation of the notification. + * + * @param mixed $notifiable + * @return \Illuminate\Notifications\Messages\MailMessage + */ + public function toMail($notifiable) + { + return (new MailMessage) + ->line('The introduction to the notification.') + ->action('Notification Action', url('/')) + ->line('Thank you for using our application!'); + } + + /** + * Get the array representation of the notification. + * + * @param mixed $notifiable + * @return array + */ + public function toArray($notifiable) + { + $transaction = $this->transaction; + return [ + "title" => "Pesanan sedang dalam pengiriman!", + "body" => "Pesanan $transaction->number sedang dikirim menuju alamat tujuan", + "type" => "Info", + "data" => $this->transaction, + "model" => get_class($this->transaction) + ]; + } + + public function toFcm($notifiable) + { + $payload = $this->toArray($notifiable); + + return [ + "notification" => [ + "title" => $payload["title"], + "body" => $payload["body"], + ] + ]; + } +} diff --git a/app/Notifications/Member/Transaction/OrderPaid.php b/app/Notifications/Member/Transaction/OrderPaid.php new file mode 100644 index 0000000..a603a95 --- /dev/null +++ b/app/Notifications/Member/Transaction/OrderPaid.php @@ -0,0 +1,83 @@ +transaction = $transaction; + } + + /** + * Get the notification's delivery channels. + * + * @param mixed $notifiable + * @return array + */ + public function via($notifiable) + { + return ['database',FcmChannel::class]; + } + + /** + * Get the mail representation of the notification. + * + * @param mixed $notifiable + * @return \Illuminate\Notifications\Messages\MailMessage + */ + public function toMail($notifiable) + { + return (new MailMessage) + ->line('The introduction to the notification.') + ->action('Notification Action', url('/')) + ->line('Thank you for using our application!'); + } + + /** + * Get the array representation of the notification. + * + * @param mixed $notifiable + * @return array + */ + public function toArray($notifiable) + { + $transaction = $this->transaction; + return [ + "title" => "Pembayaran Berhasil", + "body" => "Terima kasih, Pesanan $transaction->number akan segera kami proses", + "type" => "Info", + "data" => $this->transaction, + "model" => get_class($this->transaction) + ]; + } + + public function toFcm($notifiable) + { + $payload = $this->toArray($notifiable); + + return [ + "notification" => [ + "title" => $payload["title"], + "body" => $payload["body"], + ] + ]; + } +} diff --git a/app/Notifications/Member/Transaction/OrderProcessed.php b/app/Notifications/Member/Transaction/OrderProcessed.php new file mode 100644 index 0000000..edda130 --- /dev/null +++ b/app/Notifications/Member/Transaction/OrderProcessed.php @@ -0,0 +1,83 @@ +transaction = $transaction; + } + + /** + * Get the notification's delivery channels. + * + * @param mixed $notifiable + * @return array + */ + public function via($notifiable) + { + return ['database',FcmChannel::class]; + } + + /** + * Get the mail representation of the notification. + * + * @param mixed $notifiable + * @return \Illuminate\Notifications\Messages\MailMessage + */ + public function toMail($notifiable) + { + return (new MailMessage) + ->line('The introduction to the notification.') + ->action('Notification Action', url('/')) + ->line('Thank you for using our application!'); + } + + /** + * Get the array representation of the notification. + * + * @param mixed $notifiable + * @return array + */ + public function toArray($notifiable) + { + $transaction = $this->transaction; + return [ + "title" => "Pesanan telah diproses!", + "body" => "Pesanan $transaction->number telah diproses akan segera dikirim", + "type" => "Info", + "data" => $this->transaction, + "model" => get_class($this->transaction) + ]; + } + + public function toFcm($notifiable) + { + $payload = $this->toArray($notifiable); + + return [ + "notification" => [ + "title" => $payload["title"], + "body" => $payload["body"], + ] + ]; + } +} diff --git a/app/Notifications/Member/Transaction/OrderWaitPayment.php b/app/Notifications/Member/Transaction/OrderWaitPayment.php new file mode 100644 index 0000000..8542b94 --- /dev/null +++ b/app/Notifications/Member/Transaction/OrderWaitPayment.php @@ -0,0 +1,95 @@ +transaction = $transaction; + } + + /** + * Get the notification's delivery channels. + * + * @param mixed $notifiable + * @return array + */ + public function via($notifiable) + { + if ($this->dontSend($notifiable)) { + return []; + } + + return ['database', + FcmChannel::class, + + ]; + } + + public function dontSend($notifiable) + { + return $this->transaction->status != 'WAIT_PAYMENT'; + } + + /** + * Get the mail representation of the notification. + * + * @param mixed $notifiable + * @return \Illuminate\Notifications\Messages\MailMessage + */ + public function toMail($notifiable) + { + return (new MailMessage) + ->line('The introduction to the notification.') + ->action('Notification Action', url('/')) + ->line('Thank you for using our application!'); + } + + /** + * Get the array representation of the notification. + * + * @param mixed $notifiable + * @return array + */ + public function toArray($notifiable) + { + $transaction = $this->transaction; + + return [ + 'title' => 'Menunggu Pembayaran', + 'body' => "Silahkan lakukan pembayaran untuk pesanan $transaction->number", + 'type' => 'Info', + 'data' => $this->transaction, + 'model' => get_class($this->transaction), + ]; + } + + public function toFcm($notifiable) + { + $payload = $this->toArray($notifiable); + + return [ + 'notification' => [ + 'title' => $payload['title'], + 'body' => $payload['body'], + ], + ]; + } +} diff --git a/app/Repositories/Crm/SurveyRepository.php b/app/Repositories/Crm/SurveyRepository.php new file mode 100644 index 0000000..7c087cd --- /dev/null +++ b/app/Repositories/Crm/SurveyRepository.php @@ -0,0 +1,219 @@ +voucherEventRepository = $voucherEventRepository; + } + + public function getList(array $params = []) + { + $limit = @$params["limit"] ? (int) @$params["limit"] : 10; + $sortColumn = @$params["sort"]["column"] ? $params["sort"]["column"] : "id"; + $sortDir = @$params["sort"]["dir"] ? $params["sort"]["dir"] : "desc"; + + $results = Survey::when(@$params["search"], function($query) use ($params){ + $query->where("name","ilike","%". $params["search"] . "%") + ->where("code","ilike","%". $params["search"] . "%"); + }) + ->orderBy($sortColumn, $sortDir) + ->paginate($limit); + + return $results; + } + + public function getListDetail(Survey $survey, array $params = []) + { + $limit = @$params["limit"] ? (int) @$params["limit"] : 10; + $sortColumn = @$params["sort"]["column"] ? $params["sort"]["column"] : "id"; + $sortDir = @$params["sort"]["dir"] ? $params["sort"]["dir"] : "desc"; + + $results = SurveyQuestion::where('survey_id', $survey->id) + ->when(@$params["search"], function($query) use ($params){ + $query->where("description","ilike","%". $params["search"] . "%"); + }) + ->orderBy($sortColumn, $sortDir) + ->paginate($limit); + + return $results; + } + + public function createFeedback(Survey $survey, array $data) { + do { + $code = md5(Str::random(40)); + } while(SurveyFeedback::where('code', $code)->first()); + + $feedback = SurveyFeedback::create([ + 'code' => $code, + 'survey_id' => $survey->id, + 'channel' => @$data['channel'], + 'customer_id' => @$data['customer_id'], + 'invoice_id' => @$data['invoice_id'], + 'ip_address' => @$data['ip_address'], + 'agent' => @$data['agent'], + 'time' => Carbon::now() + ]); + + foreach($data['details'] as $detail) { + $surveyDetail = $survey->detail->where('id', $detail['survey_question_id'])->first(); + + $feedback->detail()->create([ + 'description' => @$surveyDetail->description, + 'value' => $detail['value'], + 'survey_question_id' => $detail['survey_question_id'] + ]); + } + + if ($feedback->customer){ + $notif = new SurveyRespond($feedback); + $feedback->customer->notify($notif); + } + + return $feedback; + } + + public function generateFeedback(Survey $survey, array $data) { + do { + $code = md5(Str::random(40)); + } while(SurveyFeedback::where('code', $code)->first()); + + if (@$data['customer_id']) + { + $last_feedback = SurveyFeedback::where("customer_id", @$data['customer_id']) + ->whereDate("created_at",">=", Carbon::now()->subMonth(1)) + ->first(); + if ($last_feedback) + { + return null; + } + } + + $feedback = SurveyFeedback::create([ + 'code' => $code, + 'survey_id' => $survey->id, + 'channel' => @$data['channel'], + 'customer_id' => @$data['customer_id'], + 'invoice_id' => @$data['invoice_id'] + ]); + + return $feedback; + } + + public function create(array $data) + { + do { + $code = md5(Str::random(40)); + } while(Survey::where('code', $code)->first()); + + $item = Survey::create([ + 'name' => $data['name'], + 'code' => md5(Str::random(40)), + 'title' => @$data['title'], + 'content' => @$data['content'], + 'voucher_event_id' => @$data['voucher_event_id'] + ]); + + return $item; + } + + public function createDetail(Survey $survey, array $data) + { + $item = SurveyQuestion::create([ + 'survey_id' => $survey->id, + 'description' => $data['description'], + 'type' => $data['type'], + 'data' => $data['data'], + 'order' => $data['order'] + ]); + + return $item; + } + + public function update(Survey $item, array $data) + { + $item->update($data); + return $item; + } + + public function updateDetail(SurveyQuestion $item, array $data) + { + $item->update($data); + return $item; + } + + public function updateFeedbackDetail(SurveyFeedback $item, array $data) + { + if ($item->time == null){ + $invoice = @$item->invoice; + $voucherEvent = @$item->survey->voucherEvent; + if ($invoice && $voucherEvent){ + $expired = Carbon::now()->addDay(30); + $voucher = $this->voucherEventRepository->createVoucher($voucherEvent, $invoice->customer, $invoice->customer->user, "FREE VOUCHER SURVEY GIFT", $expired, 400000); + + if ($voucher){ + + $notif = new SurveyRespond($item, $voucher); + $invoice->customer->notify($notif); + } + } + } + $item->update([ + "time" => Carbon::now(), + 'ip_address' => @$data['ip_address'], + 'agent' => @$data['agent'], + ]); + + foreach($data['details'] as $detail) { + $feedbackDetail = $item->detail()->where('survey_question_id', $detail['survey_question_id'])->first(); + $surveyDetail = SurveyQuestion::where('id', $detail['survey_question_id'])->first(); + + if($feedbackDetail) { + $feedbackDetail->update([ + 'description' => @$surveyDetail->description, + 'value' => $detail['value'] + ]); + } else { + $item->detail()->create([ + 'survey_question_id' => $detail['survey_question_id'], + 'description' => @$surveyDetail->description, + 'value' => $detail['value'] + ]); + } + } + + return $item; + } + + public function delete(Survey $item) + { + $item->delete(); + } + + public function deleteDetail(SurveyQuestion $item) + { + $item->delete(); + } + + public function findBy($column, $value) + { + $item = Survey::where($column, $value)->firstOrFail(); + return $item; + } + +} diff --git a/app/Repositories/Member/Cart/MemberCartRepository.php b/app/Repositories/Member/Cart/MemberCartRepository.php index ad986d2..2c4b7a0 100644 --- a/app/Repositories/Member/Cart/MemberCartRepository.php +++ b/app/Repositories/Member/Cart/MemberCartRepository.php @@ -16,6 +16,18 @@ class MemberCartRepository ->sum('qty'); } + static public function getSubtotal($locationId = null) + { + $location = $locationId ?? session('location_id', 22); + return Cart::where('user_id', auth()->user()->id) + ->where('location_id', $location) + ->get() + ->map(function($row){ + return $row->qty * $row->display_price; + }) + ->sum(); + } + static public function clearAll($locationId = null) { $location = $locationId ?? session('location_id', 22); diff --git a/app/Repositories/Member/ShippingRepository.php b/app/Repositories/Member/ShippingRepository.php new file mode 100644 index 0000000..9de053d --- /dev/null +++ b/app/Repositories/Member/ShippingRepository.php @@ -0,0 +1,255 @@ +biteship = $biteship; + } + + public function calcTotal($params){ + $user = auth()->user(); + $location = Location::findOrFail(@$params["location_id"]); + $items = Cart::where("user_id", $user->id) + ->where("location_id", $location->id) + ->get(); + $total_amount = 0; + foreach($items as $detail){ + $convertion = 1; + if ($detail->item->display_unit and $detail->item->display_unit != $detail->item->unit){ + $convertions = DB::select("select to_qty / from_qty as conv from item_convertions where from_unit = ? and to_unit = ?", +[$detail->item->display_unit, $detail->item->unit]); + $convertion = max((float) @$convertions[0]->conv,1); + } + $d_price = @$detail->itemReference->discount->price ?? 0; + $s_price = @$detail->itemReference->price->price ?? 0; + $price = ( $s_price ? $s_price : $detail->item->net_price) * $convertion; + $price = ( $d_price ? $d_price : $price) * $convertion; + $total = $detail->qty * $price; + $total_amount = $total_amount + $total; + } + return $total_amount; + + } + public function getList($params) + { + $biteship = $this->biteship; + $location = Location::findOrFail(@$params["location_id"]); + $address = Address::findOrFail(@$params["address_id"]); + + if (!$location->postal_code){ + throw ValidationException::withMessages([ + "location_id" => "Data gudang tidak memiliki informasi kode pos" + ]); + } + + if (!$location->latitude || !$location->longitude){ + throw ValidationException::withMessages([ + "location_id" => "Data gudang tidak memiliki informasi lat long" + ]); + } + + $hasLatLong = $address->latitude != null && $address->longitude != null; + $items = collect(@$params["items"] ?? []); + + + $user = auth()->user(); + + $items = collect(@$data["items"] ?? []); + + $items = Cart::where("user_id", $user->id) + ->where("location_id", $location->id) + ->when(count($items) > 0, function($query) use ($items){ + $query->whereIn("item_reference_id", $items->pluck("item_reference_id")); + }) + ->get(); + + $items = $items->map(function($cart){ + if (((float) @$cart->item->weight) <= 0){ + throw ValidationException::withMessages([ + "location_id" => "Berat ada yang kosong" + ]); + } + return [ + "weight" => max(@$cart->item->weight,0.001), + "quantity" => @$cart->qty, + "value" => @$cart->item->net_price, + "description" => @$cart->itemVariant->description ?? @$cart->item->name, + "name" => @$cart->item->category->name ?? "GOODS" + ]; + }); + + if ($hasLatLong){ + return $biteship->rateByLatLong([ + "origin_latitude" => $location->latitude, + "origin_longitude" => $location->longitude, + "destination_latitude" => $address->latitude, + "destination_longitude" => $address->longitude, + "items" => $items + ]); + }else{ + return $biteship->rateByPostal([ + "origin_postal_code" => $location->postal_code, + "destination_postal_code" => $address->postal_code, + "items" => $items + ]); + } + } + + + public function order(Transaction $transaction) + { + $biteship = $this->biteship; + $location = $transaction->location; + $address = $transaction->address; + + if (!$location->postal_code){ + throw ValidationException::withMessages([ + "location_id" => "Data gudang tidak memiliki informasi kode pos" + ]); + } + + if (!$location->latitude || !$location->longitude){ + throw ValidationException::withMessages([ + "location_id" => "Data gudang tidak memiliki informasi lat long" + ]); + } + + $hasLatLong = $address->latitude != null && $address->longitude != null; + $user = auth()->user(); + $items = $transaction->details; + $items = $items->map(function($cart){ + if (((float) @$cart->item->weight) == 0){ + throw ValidationException::withMessages([ + "items" => "Berat item wajib diisi!" + ]); + } + + return [ + "weight" => max(@$cart->item->weight,0.001), + "quantity" => @$cart->qty, + "value" => @$cart->item->net_price, + "description" => @$cart->itemVariant->description ?? @$cart->item->name, + "name" => @$cart->item->category->name ?? "GOODS" + ]; + }); + $subtotal = $items->reduce(function($acc, $cart){ + return $acc + ( @$cart->qty * @$cart->item->net_price); + },0); + + if ($hasLatLong){ + return $biteship->orderByLatLong( + [ + "origin_contact_name" => $location->display_name, + "origin_contact_phone" => $location->phone, + "origin_address" => $location->address, + "origin_latitude" => $location->latitude, + "origin_longitude" => $location->longitude, + + "destination_contact_name" => $address->name, + "destination_contact_phone" => $address->phone, + "destination_latitude" => $address->latitude, + "destination_longitude" => $address->longitude, + "destination_address" => $address->address, + + "reference_id" => $transaction->number, + + "courier_insurance" => $subtotal, + "courier_company" => $transaction->courier_company, + "courier_type" => $transaction->courier_type, + "items" => $items + ] + ); + }else{ + $destination_address = $address->address; + if (@$address->subdistrict->name) + $destination_address .= "," .@$address->subdistrict->name; + if (@$address->district->name) + $destination_address .= "," .@$address->district->name; + if (@$address->city->name) + $destination_address .= "," .@$address->city->name; + if (@$address->province->name) + $destination_address .= "," .@$address->province->name; + return $biteship->orderByPostal( + [ + "origin_contact_name" => $location->display_name, + "origin_contact_phone" => $location->phone, + "origin_address" => $location->address, + "origin_postal_code" => $location->postal_code, + "origin_latitude" => $location->latitude, + "origin_longitude" => $location->longitude, + + "destination_contact_name" => $address->name, + "destination_contact_phone" => $address->phone, + "destination_address" => $destination_address, + "destination_postal_code" => $address->postal_code, + + "reference_id" => $transaction->number, + "courier_insurance" => $subtotal, + + "courier_company" => $transaction->courier_company, + "courier_type" => $transaction->courier_type, + "items" => $items + ] + ); + } + } + + public function tracking(Transaction $transaction, $waybill = true){ + $biteship = $this->biteship; + $shipping = $transaction->shipping; + + if (!@$shipping){ + + throw ValidationException::withMessages([ + "shipping" => "Belum ada process pengiriman!" + ]); + + if ($waybill){ + + $data = (array) $biteship->trackingByWaybill([ + "id" => $shipping->tracking_id, + "waybill_id" => $shipping->waybill_id, + "courier" => $shipping->courier, + ]); + }else{ + + $data = (array) $biteship->trackingById([ + "id" => $shipping->tracking_id, + "waybill_id" => $shipping->waybill_id, + "courier" => $shipping->courier, + ]); + } + + $shipping->tracks()->delete(); + foreach($data["history"] as $history){ + $history = (array) $history; + $shipping->tracks()->create([ + "status" => $history["status"], + "note" => $history["note"], + "created_at" => @$history["updated_at"] + ]); + } + $shipping->fill([ + "status" => $data["status"] + ]); + $shipping->update(); + + } + } +} diff --git a/app/Repositories/Member/Transaction/CheckoutController.php b/app/Repositories/Member/Transaction/CheckoutController.php new file mode 100644 index 0000000..5cb9a3e --- /dev/null +++ b/app/Repositories/Member/Transaction/CheckoutController.php @@ -0,0 +1,26 @@ +validated(); + $item = $repository->create($data); + + $notification = new OrderWaitPayment($item); + $user = auth()->user(); + $user->notify($notification->delay(now()->addMinutes(1))); + + return new CheckoutResource($item); + } + +} diff --git a/app/Repositories/Member/Transaction/TransactionRepository.php b/app/Repositories/Member/Transaction/TransactionRepository.php new file mode 100644 index 0000000..92e8ec7 --- /dev/null +++ b/app/Repositories/Member/Transaction/TransactionRepository.php @@ -0,0 +1,872 @@ +shippingRepository = $shippingRepository; + $this->invoiceRepository = $invoiceRepository; + $this->roleRepository = $roleRepository; + $this->voucherRepository = $voucherRepository; + $this->xendit = $xendit; + } + + public function getList($params = []) + { + $customColumns = [ + 'user.name' => 'users.name', + ]; + + $limit = @$params["limit"] ? (int) @$params["limit"] : 10; + $sortColumn = @$params["sort"]["column"] ? (@$customColumns[$params["sort"]["column"]] ? $customColumns[$params["sort"]["column"]] : $params["sort"]["column"]) : "id"; + $sortDir = @$params["sort"]["dir"] ? $params["sort"]["dir"] : "desc"; + + $model = Transaction::select('transactions.*') + ->leftJoin('address', 'address.id', 'transactions.address_id') + ->leftJoin('users', 'users.id', 'transactions.user_id') + ->when(@!$params['is_admin'], function ($query) { + $query->where('transactions.user_id', auth()->user()->id); + }) + ->when(@$params['status'], function ($query) use ($params) { + $query->where('transactions.status', $params['status']); + }) + ->when(@$params['filter']['multiple_status'] && !empty($params['filter']['multiple_status']), function($query) use ($params){ + $query->whereIn("transactions.status", $params['filter']['multiple_status']); + }) + ->when(@$params['filter']['except'], function($query) use ($params){ + $except = @$params['filter']['except']; + if (is_array($except)){ + $query->whereNotIn("status", $except); + }else{ + $query->where("status","<>", $except); + } + }) + ->when(@$params['filter']['start'], function($query) use ($params){ + $query->whereDate("transactions.time", ">=", $params['filter']['start']); + }) + ->when(@$params['filter']['end'], function($query) use ($params){ + $query->whereDate("transactions.time", "<=", $params['filter']['end']); + }) + ->when(@$params['search'], function($query) use ($params) { + $query->where(function($query) use ($params) { + $query->where('transactions.number', 'ilike', '%' . $params['search'] . '%') + ->orWhere('users.name', 'ilike', '%' . $params['search'] . '%') + ->orWhere('address.name', 'ilike', '%' . $params['search'] . '%'); + }); + }) + ->orderBy($sortColumn, $sortDir) + ->paginate($limit); + + return $model; + } + + public function getListForExport($params = []) + { + $customColumns = [ + 'user.name' => 'users.name', + ]; + + $limit = @$params["limit"] ? (int) @$params["limit"] : 10; + $sortColumn = @$params["sort"]["column"] ? (@$customColumns[$params["sort"]["column"]] ? $customColumns[$params["sort"]["column"]] : $params["sort"]["column"]) : "transactions.id"; + $sortDir = @$params["sort"]["dir"] ? $params["sort"]["dir"] : "desc"; + + return Transaction::selectRaw(" + transactions.*, + transaction_details.*, + coalesce(item_variants.description, items.name) as product_name, + item_reference.number as item_reference_number + ") + ->leftJoin('address', 'address.id', 'transactions.address_id') + ->leftJoin('users', 'users.id', 'transactions.user_id') + ->leftJoin('transaction_details', 'transaction_details.transaction_id', 'transactions.id') + ->leftJoin('item_reference', 'item_reference.id', 'transaction_details.item_reference_id') + ->leftJoin('items', 'items.id', 'item_reference.item_id') + ->leftJoin('item_variants', 'item_variants.id', 'transaction_details.item_variant_id') + ->when(@!$params['is_admin'], function ($query) { + $query->where('transactions.user_id', auth()->user()->id); + }) + ->when(@$params['status'], function ($query) use ($params) { + $query->where('transactions.status', $params['status']); + }) + ->when(@$params['filter']['multiple_status'] && !empty($params['filter']['multiple_status']), function($query) use ($params){ + $query->whereIn("transactions.status", $params['filter']['multiple_status']); + }) + ->when(@$params['filter']['except'], function($query) use ($params){ + $except = @$params['filter']['except']; + if (is_array($except)){ + $query->whereNotIn("status", $except); + }else{ + $query->where("status","<>", $except); + } + }) + ->when(@$params['filter']['start'], function($query) use ($params){ + $query->whereDate("transactions.time", ">=", $params['filter']['start']); + }) + ->when(@$params['filter']['end'], function($query) use ($params){ + $query->whereDate("transactions.time", "<=", $params['filter']['end']); + }) + ->when(@$params['search'], function($query) use ($params) { + $query->where(function($query) use ($params) { + $query->where('transactions.number', 'ilike', '%' . $params['search'] . '%') + ->orWhere('users.name', 'ilike', '%' . $params['search'] . '%') + ->orWhere('address.name', 'ilike', '%' . $params['search'] . '%'); + }); + }) + ->orderBy($sortColumn, $sortDir) + ->get(); + } + + public function getCount($params = []) + { + $model = Transaction::select('transactions.*') + ->leftJoin('address', 'address.id', 'transactions.address_id') + ->leftJoin('users', 'users.id', 'transactions.user_id') + ->when(@!$params['is_admin'], function ($query) { + $query->where('transactions.user_id', auth()->user()->id); + }) + ->when(@$params['status'], function ($query) use ($params) { + $query->where('transactions.status', $params['status']); + }) + ->when(@$params['filter']['multiple_status'] && !empty($params['filter']['multiple_status']), function($query) use ($params){ + $query->whereIn("transactions.status", $params['filter']['multiple_status']); + }) + ->when(@$params['filter']['except'], function($query) use ($params){ + $except = @$params['filter']['except']; + if (is_array($except)){ + $query->whereNotIn("status", $except); + }else{ + $query->where("status","<>", $except); + } + }) + ->select("status") + ->addSelect(DB::raw("COUNT(*) as count")) + ->groupBy("status") + ->get(); + + return $model; + } + + public function getlistItem(Transaction $transaction, $params = []) { + + $limit = @$params["limit"] ? (int) @$params["limit"] : 10; + $sortColumn = @$params["sort"]["column"] ? $params["sort"]["column"] : "id"; + $sortDir = @$params["sort"]["dir"] ? $params["sort"]["dir"] : "desc"; + + return TransactionDetail::select('transaction_details.*') + ->leftJoin('item_reference', 'item_reference.id', 'transaction_details.item_reference_id') + ->leftJoin('item_variants', 'item_variants.id', 'transaction_details.item_variant_id') + ->where('transaction_id', $transaction->id) + ->when(@$params['search'], function($query) use ($params) { + $query->where(function($query) use ($params) { + $query->where('item_variants.description', 'ilike', '%' . $params['search'] . '%') + ->orWhere('item_reference.number', 'ilike', '%' . $params['search'] . '%'); + }); + }) + ->orderBy($sortColumn, $sortDir) + ->paginate($limit); + } + + public function setStatusTransaction($transactionId, $status, $note = '') + { + $user = auth()->user(); + TransactionStatus::create([ + 'transaction_id' => $transactionId, + 'time' => now(), + 'note' => $note, + 'status' => $status, + 'user_id' => @$user->id ?? 0, + ]); + } + + public function getAmount($items) + { + $amount = 0; + + foreach ($items as $detail) + { + $itemReference = ItemReference::findOrFail($detail['item_reference_id']); + $item = Items::findOrFail($itemReference->item_id); + $amount += $detail['qty'] * $item->unit_price; + } + + return $amount; + } + + + public function createPayment($model){ + + $fees = []; + if ($model->shipping_price){ + $fees[] = + [ + "type" => "SHIPPING", + "value" => $model->shipping_price + ]; + } + if ($model->discount){ + $fees[] = + [ + "type" => "DISCOUNT", + "value" => $model->discount + ]; + } + + $remaining_amount = ($model->subtotal + $model->shipping_price) - $model->discount; + + $xendit_response = $this->xendit->createPaymentLink([ + "external_id" => $model->number, + "amount" => $remaining_amount, + "items" => $model->details->map(function($cart){ + return [ + "name" => $cart->item->name, + "category" => @$cart->item->category->name ?? "GOODS", + "price" => $cart->unit_price, + "quantity" => $cart->qty + ]; + }), + "fees" => $fees + ]); + + + if ($xendit_response){ + $xendit_link = XenditLink::create([ + "uid" => $xendit_response["id"], + "invoice_url" => $xendit_response["invoice_url"], + "user_id" => $xendit_response["user_id"], + "external_id" => $xendit_response["external_id"], + "status" => $xendit_response["status"], + "amount" => $xendit_response["amount"], + "expiry_date" => $xendit_response["expiry_date"] + ]); + $payment = TransactionPayment::create([ + "amount" => $remaining_amount, + "status" => "PENDING", + "transaction_id" => $model->id, + "method_type" => get_class($xendit_link), + "method_id" => $xendit_link->id + ]); + } + $this->setStatusTransaction($model->id, 'WAIT_PAYMENT'); + return $xendit_response; + } + + + public function create($data) + { + $model = DB::transaction(function () use ($data) { + $user = auth()->user(); + $customer = Customer::where("user_id", $user->id)->first(); + $location_id = $data["location_id"]; + + + // validasi voucher + if ( @$data["vouchers"]){ + $used_voucher = Voucher::whereIn("id", $data["vouchers"])->whereNotNull("used_at")->count(); + if ($used_voucher){ + throw ValidationException::withMessages([ + "voucher" => "Voucher Telah digunakan " + ]); + } + } + + // get numbering + $auto = new AutoNumbering([ + "type" => "TRX", + "location_id" => 0, + "prefix" => "TRX", + "pad" => 12 + ]); + $number = $auto->getCurrent(); + $data["number"] = $number; + + $items = collect(@$data["items"] ?? []); + + $carts = Cart::where('user_id', $user->id) + ->where("location_id", $data["location_id"]) +->when(count($items) > 0, function($query) use ($items) { + $query->whereIn("item_reference_id", $items->pluck("item_reference_id")); +}) + ->get(); + $carts = $carts->map(function($cart) use ($items){ + $item = $items->firstWhere("item_reference_id", $cart->item_reference_id); + if ($item){ + $cart->new_qty = $cart->qty - $item["qty"]; + $cart->qty = $item["qty"]; + } + return $cart; + }); + + if (count($carts) == 0){ + throw ValidationException::withMessages([ + "items" => "Tidak ada item di keranjang" + ]); + } + + // detail data process + $subtotal = 0; + $details = []; + foreach ($carts as $detail) + { + $convertion = 1; + if (@$detail->item->display_unit and @$detail->item->display_unit != @$detail->item->unit){ + $convertions = DB::select("select to_qty / from_qty as conv from item_convertions where from_unit = ? and to_unit = ?", [$detail->item->display_unit, $detail->item->unit] ); + $convertion = max((float) @$convertions[0]->conv, 1); + } + $d_price = @$detail->itemReference->discount->price ?? 0; + $s_price = @$detail->itemReference->price->price ?? 0; + $price = ( $s_price ? $s_price : $detail->item->net_price) * $convertion; + $price = ( $d_price ? $d_price : $price) * $convertion; + $total = $detail->qty * $price; + $details[] = [ + 'item_id' => $detail->item_id, + 'item_variant_id' => $detail->item_variant_id, + 'item_reference_id' => $detail->item_reference_id, + 'qty' => $detail->qty, + 'unit' => ($detail->item->display_unit ?? $detail->item->unit), + 'unit_price' => $price, + 'unit_cost' => $detail->item->unit_cost, + 'total' => $total + ]; + $subtotal += $total; + } + + + // validasi voucher + if ( @$data["vouchers"]){ + $used_vouchers = Voucher::whereIn("id", $data["vouchers"])->get(); + foreach($used_vouchers as $v){ + if ($subtotal < ((float) $v->min_sales)){ + throw ValidationException::withMessages([ + "voucher" => "Voucher minimal belanja ".$v->min_sales + ]); + } + } + } + + + // get shipping_price + $list_shipping = $this->shippingRepository->getList($data); + $filtered_shipping = collect($list_shipping["pricing"])->filter(function($pricing) use ($data){ + return $pricing["company"] == $data["courier_company"] && + $pricing["type"] == $data["courier_type"]; + })->values(); + if (count($filtered_shipping) == 0){ + throw ValidationException::withMessages([ + "courier_company" => "Pilihan kurir tidak ditemukan" + ]); + } +if ($subtotal >= 1000000){ + $shipping_price = max($filtered_shipping[0]["price"] - 50000, 0); +}else{ + $shipping_price = $filtered_shipping[0]["price"]; +} + $valid_vouchers = $this->voucherRepository->getValidByItems($carts, $customer->id); + $vouchers = @$data["vouchers"] ? Voucher::whereIn("id", $data["vouchers"]) + ->whereIn("id",$valid_vouchers) + ->get(): collect([]); + + // discount + $discount = $vouchers->reduce(function($acc, $voucher){ + return $acc + ($voucher->nominal); + },0); + + $subtotal2 = $subtotal + $shipping_price; + $amount = max(0, $subtotal2 - $discount); + $discount = min($subtotal2, $discount); + + + // discount points + $discount_point = 0; + $use_customer_points = @$data["use_customer_points"] ?? 0; + if ($use_customer_points > 0){ + $current_point = auth()->user()->customer->point; + + // use_customer_points + if ($use_customer_points > $current_point){ + throw ValidationException::withMessages([ + "use_customer_points" => "Point tidak cukup. Poin anda " . number_format($current_point, 0, ",", ".") + ]); + } + + + // convert point to amount + $point_to_amount = $use_customer_points * 1000; + + $discount_point = min($point_to_amount, $amount); + + $amount = max(0, $amount - $discount_point); + } + + + $model = Transaction::create([ + 'number' => $data['number'], + 'address_id' => $data['address_id'], + 'location_id' => $data['location_id'], + 'courier_company' => $data['courier_company'], + 'courier_type' => $data['courier_type'], + 'shipping_price' => $shipping_price, + 'user_id' => auth()->user()->id, + 'customer_id' => @$customer->id, + 'time' => now(), + 'subtotal' => $subtotal, + 'discount' => $discount, + 'point' => $discount_point, + 'amount' => $amount, + 'status' => 'WAIT_PAYMENT', + ]); + $model->details()->createMany($details); + + + $subtotal_remaining = $subtotal2; + foreach ($vouchers as $voucher) { + if ($subtotal_remaining <= 0){ + continue; + } + + $voucher->fill([ + "used_at" => Carbon::now(), + "reference_used_id" => $model->id, + "reference_used_type" => get_class($model) + ]); + $voucher->save(); + + TransactionPayment::create([ + "amount" => min($voucher->nominal, $subtotal_remaining), + "status" => "PAID", + "transaction_id" => $model->id, + "method_type" => get_class($voucher), + "method_id" => $voucher->id + ]); + + $subtotal_remaining = max(0,$subtotal_remaining - $voucher->nominal); + } + + if ($use_customer_points > 0){ + $cp = CustomerPoint::create([ + "customer_id" => $model->customer_id, + "point" => -1 * $use_customer_points, + "reference_type" => get_class($model), + "reference_id" => $model->id, + "description" => "Discount Point", + ]); + + $current_point = auth()->user()->customer->point; + + if ($current_point < 0){ + throw ValidationException::withMessages([ + "use_customer_points" => "Point anda telah habis" + ]); + } + + TransactionPayment::create([ + "amount" => $discount_point, + "status" => "PAID", + "transaction_id" => $model->id, + "method_type" => get_class($cp), + "method_id" => $cp->id + ]); + } + + // create payment + $fees = [ + [ + "type" => "SHIPPING", + "value" => $shipping_price + ] + ]; + if ($discount){ + $fees[] = + [ + "type" => "DISCOUNT", + "value" => $discount + ]; + } + $remaining_amount = $amount; + if ($amount > 0){ + $xendit_response = $this->xendit->createPaymentLink([ + "external_id" => $model->number, + "amount" => $remaining_amount, + "items" => $carts->map(function($cart){ +$detail = $cart; + $convertion = 1; + if (@$detail->item->display_unit and @$detail->item->display_unit != @$detail->item->unit){ + $convertions = DB::select("select to_qty / from_qty as conv from item_convertions where from_unit = ? and to_unit = ?", [$detail->item->display_unit, $detail->item->unit] ); + $convertion = max((float) @$convertions[0]->conv, 1); + } + + $d_price = @$detail->itemReference->discount->price ?? 0; + $s_price = @$detail->itemReference->price->price ?? 0; + $price = ( $s_price ? $s_price : $detail->item->net_price) * $convertion; + $price = ( $d_price ? $d_price : $price) * $convertion; + + return [ + "name" => $cart->item->name, + "category" => @$cart->item->category->name ?? "GOODS", + "price" => $price, + "quantity" => $cart->qty + ]; + }), + "fees" => $fees + ]); + + if ($xendit_response){ + + $xendit_link = XenditLink::create([ + "uid" => $xendit_response["id"], + "invoice_url" => $xendit_response["invoice_url"], + "user_id" => $xendit_response["user_id"], + "external_id" => $xendit_response["external_id"], + "status" => $xendit_response["status"], + "amount" => $xendit_response["amount"], + "expiry_date" => $xendit_response["expiry_date"] + ]); + + $payment = TransactionPayment::create([ + "amount" => $remaining_amount, + "status" => "PENDING", + "transaction_id" => $model->id, + "method_type" => get_class($xendit_link), + "method_id" => $xendit_link->id + ]); + } + + $this->setStatusTransaction($model->id, 'WAIT_PAYMENT'); + }else{ + $model->status = 'WAIT_PROCESS'; + $model->save(); + $this->setStatusTransaction($model->id, 'WAIT_PROCESS'); + } + + + // clear cart + + foreach($carts as $cart){ + if ($cart->new_qty <= 0){ + $cart->delete(); + }else{ + $new_qty = $cart->new_qty; + unset($cart->new_qty); + $cart->update([ + "qty" => $new_qty + ]); + } + } + + return $model; + }); + + return $model; + } + + public function webhookPayment($data) + { + $paymentLink = XenditLink::where("uid", $data["id"])->firstOrFail(); + $paymentLink->fill([ + "payment_id" => @$data["payment_id"], + "paid_amount" => @$data["paid_amount"], + "payment_method" => @$data["payment_method"], + "bank_code" => @$data["bank_code"], + "payment_channel" => @$data["payment_channel"], + "payment_destination" => @$data["payment_destination"], + "received_amount" => @$data["adjusted_received_amount"], + "status" => @$data["status"], + "paid_at" => @$data["paid_at"] + ]); + $paymentLink->save(); + $paymentLink->payment()->update([ + "status" => $paymentLink->status, + ]); + + $transaction = Transaction::find($paymentLink->payment->transaction_id); + if ($transaction && @$data["status"] == "PAID"){ + + if ($transaction->status != "WAIT_PROCESS"){ // once notify for first + $roleRepository = $this->roleRepository; + $users = $roleRepository->findUserByPermission("transaction.online"); + $notification = new NewOrder($transaction); + Notification::send($users, $notification); + + $notification2 = new OrderPaid($transaction); + $transaction->customer->user->notify($notification2); + } + + $this->setStatusTransaction($transaction->id,'WAIT_PROCESS'); + $transaction->status = 'WAIT_PROCESS'; + $transaction->save(); + }else if ($transaction && @$data["status"] == "EXPIRED"){ + + if ($transaction->status != "CANCELED"){ + $this->cancel($transaction->id,"Oleh System"); + } + } + + return $paymentLink; + } + + public function payment($id) + { + $model = DB::transaction(function () use ($id) { + + $model = Transaction::findOrfail($id); + + TransactionPayment::create([ + 'transaction_id' => $model->id, + 'amount' => $model->amount, + 'status' => 'PAID', + ]); + + $this->setStatusTransaction($model->id, 'WAIT_PROCESS'); + + $model->status = 'WAIT_PROCESS'; + $model->save(); + + return $model; + }); + + return $model; + } + + public function process($id) + { + $repository = $this->invoiceRepository; + $model = DB::transaction(function () use ($id, $repository) { + + $model = Transaction::findOrfail($id); + + $this->setStatusTransaction($model->id, 'ON_PROCESS'); + + $model->status = 'ON_PROCESS'; + $model->save(); + + $invoice = $this->invoiceRepository->createFromOnline($model); + + $vouchers = $model->vouchers; + foreach ($vouchers as $voucher) { + $repository->checkAffiliatorVoucher($invoice, $voucher); + } + + return $model; + }); + + $user = @$model->customer->user; + if ($user){ + $notification2 = new OrderProcessed($model); + $user->notify($notification2); + } + + return $model; + } + + public function deliver($id) + { + $model = DB::transaction(function () use ($id) { + + $model = Transaction::findOrfail($id); + + $shipping = TransactionShipping::where("transaction_id",$model->id)->first(); + if ($shipping){ + return $model; + } + // order shippping + $data = $this->shippingRepository->order($model); + + if(empty($data)) { + throw new Exception('error failed create order'); + } + + $weight_total = collect($data["items"])->reduce(function($acc, $item){ + $item = (object) $item; + return $acc + ($item->weight * $item->quantity); + },0); + + $shipping = TransactionShipping::create([ + "transaction_id" => $model->id, + + "origin_address" => $data["origin"]["address"], + "origin_name" => $data["origin"]["contact_name"], + "origin_phone" => $data["origin"]["contact_phone"], + "origin_postal_code" => $data["origin"]["postal_code"], + "origin_latitude" => $data["origin"]["coordinate"]["latitude"], + "origin_longitude" => $data["origin"]["coordinate"]["longitude"], + "origin_note" => $data["origin"]["note"], + + + "destination_address" => @$data["destination"]["address"], + "destination_name" => @$data["destination"]["contact_name"], + "destination_phone" => @$data["destination"]["contact_phone"], + "destination_postal_code" => @$data["destination"]["postal_code"], + "destination_latitude" => @$data["destination"]["coordinate"]["latitude"], + "destination_longitude" => @$data["destination"]["coordinate"]["longitude"], + "destination_note" => @$data["destination"]["note"], + + "shipper_name" => @$data["shipper"]["name"], + "shipper_phone" => @$data["shipper"]["phone"], + "shipper_email" => @$data["shipper"]["email"], + + "uid" => $data["id"], + "tracking_id" => @$data["courier"]["tracking_id"], + "waybill_id" => @$data["courier"]["waybill_id"], + "company" => @$data["courier"]["company"], + "driver_name" => @$data["courier"]["driver_name"], + "driver_phone" => @$data["courier"]["driver_phone"], + "driver_photo_url" => @$data["courier"]["driver_photo_url"], + "driver_plate_number" => @$data["courier"]["driver_plate_number"], + "type" => @$data["courier"]["type"], + "insurance_amount" => @$data["insurance"]["amount"], + "insurance_fee" => @$data["insurance"]["fee"], + "weight_total" => $weight_total, + "price" => @$data["price"], + "note" => @$data["note"], + "status" => @$data["status"] + ]); + + $this->setStatusTransaction($model->id, 'ON_DELIVERY'); + + $model->status = 'ON_DELIVERY'; + $model->save(); + + + return $model; + }); + + + $user = @$model->customer->user; + if ($user){ + $notification2 = new OrderOnDelivery($model); + $user->notify($notification2); + } + + return $model; + } + + public function close($id) + { + $model = DB::transaction(function () use ($id) { + + $model = Transaction::findOrfail($id); + + $this->setStatusTransaction($model->id, 'CLOSED'); + + $model->status = 'CLOSED'; + $model->save(); + + return $model; + }); + + return $model; + } + + public function delivered($id) + { + $model = DB::transaction(function () use ($id) { + + $model = Transaction::findOrfail($id); + + $this->setStatusTransaction($model->id, 'DELIVERED'); + + $model->status = 'DELIVERED'; + $model->save(); + + return $model; + }); + + $user = @$model->customer->user; + if ($user){ + $notification2 = new OrderDelivered($model); + $user->notify($notification2); + } + + return $model; + } + + public function cancel($id, $note = "") + { + $model = DB::transaction(function () use ($id, $note) { + + $model = Transaction::findOrfail($id); + $model->vouchers()->update([ + "used_at" => null, + "reference_used_id" => null, + "reference_used_type" => null + ]); + + $this->setStatusTransaction($model->id, 'CANCELED', $note); + + $model->status = 'CANCELED'; + $model->save(); + + + + + $customer_point = CustomerPoint::where("customer_id", $model->customer_id) + ->where("reference_id", $model->id) + ->where("reference_type", get_class($model)) + ->orderBy("id", "asc") + ->first(); + + if ($customer_point){ + CustomerPoint::create([ + "customer_id" => $model->customer_id, + "point" => -1 * $customer_point->point, + "reference_type" => get_class($model), + "reference_id" => $model->id, + "description" => "Refund Discount Point", + ]); + } + + + return $model; + }); + + $user = @$model->customer->user; + if ($user){ + $notification2 = new OrderCanceled($model); + $user->notify($notification2); + } + + return $model; + } +} diff --git a/app/Repositories/Pos/InvoiceRepository.php b/app/Repositories/Pos/InvoiceRepository.php new file mode 100644 index 0000000..a29155b --- /dev/null +++ b/app/Repositories/Pos/InvoiceRepository.php @@ -0,0 +1,842 @@ +surveyRepository = $surveyRepository; + } + + public function getList(array $params = [], $all = false) + { + $user = auth()->user(); + $location_id = @$user->employee->location_id; + + $sortColumn = [ + 'number' => 'pos_invoices.number', + 'time' => 'pos_invoices.time', + 'total' => 'pos_invoices.total', + 'location.name' => 'locations.name', + 'customer' => 'customers.name', + 'sales.name' => 'sales.name', + 'sales2.name' => 'sales.name', + 'user.name' => 'users.name' + ]; + + $limit = @$params["limit"] ? (int) @$params["limit"] : 10; + $sortColumn = @$params["sort"]["column"] ? $sortColumn[$params["sort"]["column"]] : "pos_invoices.id"; + $sortDir = @$params["sort"]["dir"] ? $params["sort"]["dir"] : "desc"; + + $results = PosInvoice::selectRaw(" + pos_invoices.*, + CASE WHEN a.first_transaction is not null THEN 1 ELSE 0 END as new_customer, + CASE WHEN (a.first_transaction IS NOT NULL AND customers.number NOT ILIKE 'CAG%') OR customers.number = 'NONAME' THEN 'online' ELSE 'offline' END as type" + ) + ->leftJoin(DB::raw("(select min(pos_invoices.id) as first_transaction from pos_invoices group by customer_id) as a"), 'a.first_transaction', 'pos_invoices.id') + ->leftJoin('transactions', 'transactions.invoice_id', 'pos_invoices.id') + ->leftJoin('locations', 'locations.id', 'pos_invoices.location_id') + ->leftJoin('customers', 'customers.id', 'pos_invoices.customer_id') + ->leftJoin('users', 'users.id', 'pos_invoices.user_id') + ->leftJoin('sales', 'sales.id', 'pos_invoices.sales_id') + ->leftJoin('sales as sales2', 'sales2.id', 'pos_invoices.sales2_id') + ->when(@$params["search"], function ($query) use ($params) { + $query->where(function ($query) use ($params) { + $query->where("pos_invoices.number", "ilike", "%" . $params["search"] . "%") + ->orWhere("sales.name", "ilike", "%" . $params["search"] . "%") + ->orWhere("sales2.name", "ilike", "%" . $params["search"] . "%") + ->orWhere("users.name", "ilike", "%" . $params["search"] . "%") + ->orWhere("customers.name", "ilike", "%" . $params["search"] . "%") + ->orWhere("customers.phone", "ilike", "%" . $params["search"] . "%"); + /* ->orWhereExists(function($subquery) use ($params) { + $subquery->select(DB::raw(1)) + ->from('pos_invoice_details') + ->leftJoin('items', 'pos_invoice_details.item_id', 'items.id') + ->leftJoin('item_variants', 'item_variants.id', 'pos_invoice_details.item_variant_id') + ->leftJoin('item_reference', 'item_reference.id', 'pos_invoice_details.item_reference_id') + ->whereColumn('pos_invoice_details.invoice_id', 'pos_invoices.id') + ->where(function($q) use ($params) { + $q->where('item_reference.number', 'ilike', '%' . $params['search'] . '%') + ->orWhere('items.number', 'ilike', '%' . $params['search'] . '%') + ->orWhere('items.name', 'ilike', '%' . $params['search'] . '%') + ->orWhere('item_variants.description', 'ilike', '%' . $params['search'] . '%') + ->orWhere('item_variants.code', 'ilike', '%' . $params['search'] . '%'); + }); + }); */ + }); + }) + ->when(!$all, function ($query) use ($location_id) { + $query->where("pos_invoices.location_id", $location_id); + }) + ->when(@$params['active'], function($query) { + $query->whereNull('canceled_at'); + }) + ->when(@$params['filter'], function ($query) use ($params) { + foreach ($params['filter'] as $filter) { + if ($filter['column'] == 'start') { + $query->whereDate("pos_invoices.time", ">=", $filter["query"]); + } else if ($filter['column'] == 'end') { + $query->whereDate("pos_invoices.time", "<=", $filter["query"]); + } else if ($filter['column'] == 'sync') { + if ($filter['query'] == 'BERHASIL') { + $query->whereNotNull('pos_invoices.sync_at'); + } + + if ($filter['query'] == 'GAGAL') { + $query->whereNull('pos_invoices.sync_at') + ->whereNotNull('pos_invoices.sync_log'); + } + } else if ($filter['column'] == 'payment_method') { + $query->whereRaw("EXISTS (SELECT id FROM pos_invoice_payments WHERE invoice_id = pos_invoices.id AND method = '" . $filter['query'] . "' AND amount > 0)"); + } else { + $query->where('pos_invoices.' . $filter['column'], $filter['query']); + } + } + }) + ->orderBy($sortColumn, $sortDir) + ->paginate($limit); + return $results; + } + + public function getUnPosted() + { + return PosInvoice::whereNull("sync_at") + ->whereNull("canceled_at") + ->take(1000)->get(); + } + + public function createFromOnline($model) + { + DB::beginTransaction(); + try{ + $sales = Sales::where("name","ECOMMERCE")->first(); + $user = auth()->user(); + $posInvoice = PosInvoice::find((int) $model->invoice_id); + if ($posInvoice) + return; + + $location = $model->location; + $auto = new AutoNumbering([ + "type" => "POS", + "prefix" => str_replace(" ", "", $location->code), + "location_id" => $location->id, + "pad" => 12 + ]); + $number = $auto->getCurrent(); + + $invoice = new PosInvoice([ + "number" => $number, + "user_id" => $user->id, + "customer_id" => $model->customer_id, + "time" => $model->time, + "location_id" => $location->id, + "sales_id" => (int) @$sales->id, + "note" => @$model->note, + "subtotal" => $model->subtotal, + "voucher" => 0, + "discount" => 0, + "tax" => 0, + "total" => $model->subtotal + $model->shipping_price, + "vouchers" => "", + "note_internal" => $model->number, + ]); + $invoice->save(); + $model->invoice_id = $invoice->id; + $model->save(); + + foreach ($model->details as $row) { + $invoice->details()->create([ + 'item_reference_id' => $row->item_reference_id, + 'item_id' => $row->item_id, + 'item_variant_id' => $row->item_variant_id, + 'note' => '', + 'qty' => $row->qty, + 'unit_price'=> $row->unit_price, + 'unit'=> $row->unit, + 'reference'=> $row->reference->number, + 'discount'=> (int) @$row->discount, + 'vat'=> 0, + 'total'=> $row->total, + 'unit_cost'=> $row->unit_cost + ]); + } + + // find biaya expedisi + asuransi + $item = ItemReference::where("number","9CBYEXPDAS")->first(); + + if ($model->shipping_price && $item){ + $invoice->details()->create([ + 'item_reference_id' => $item->id, + 'item_id' => $item->item_id, + 'item_variant_id' => $item->item_variant_id, + 'note' => $model->courier_company." ".$model->courier_type, + 'qty' => 1, + 'unit_price' => $model->shipping_price, + 'unit' => 'PCS', + 'reference' => $item->number, + 'discount' => 0, + 'vat' => 0, + 'total'=> $model->shipping_price, + 'unit_cost'=> $model->shipping_price + ]); + } + + foreach ($model->payments as $row) { + + if ($row->method_type == XenditLink::class){ + if ($row->status == "PAID"){ + + $invoice->payments()->create([ + "method" => "XENDIT", + "amount" => $row->amount, + "bank" => "XENDIT", + "card_number" => $row->method->uid, + "remarks" => @$row->method->payment_method." ".@$row->method->bank_code + ]); + } + } + } + + foreach($model->vouchers as $voucher){ + + $invoice->payments()->create([ + "method" => "VOUCHER", + "amount" => $voucher->nominal, + "bank" => "VOUCHER", + "card_number" => $voucher->number, + "remarks" => "" + ]); + } + $model->invoice_id = $invoice->id; + $model->save(); + + $survey = Survey::where("name","AFTER PURCHASE")->first(); + if ($survey){ + $data = [ + "channel" => "pos", + "customer_id" => $invoice->customer_id, + "invoice_id" => $invoice->id + ]; + $this->surveyRepository->generateFeedback($survey, $data); + } + + DB::commit(); + + return $invoice; + }catch(\Exception $e){ + DB::rollback(); + throw $e; + } + } + + public function create(array $data) + { + return DB::transaction(function () use ($data) { + + $numbering = (array) @DB::select("SELECT id, transaction, location_id, prefix, pad, current + FROM numbering + WHERE transaction = ? AND location_id = ? + FOR UPDATE + ", ["POS", $data["location_id"]])[0]; + + $location = Location::findOrFail($data["location_id"]); + if ($numbering == null) { + $numbering = DB::table("numbering")->insert([ + "transaction" => "POS", + "location_id" => $data["location_id"], + "prefix" => str_replace(" ", "", $location->code), + "pad" => 12, + "current" => 0 + ]); + + $numbering = (array) DB::select("SELECT id, transaction, location_id, prefix, pad, current + FROM numbering + WHERE id = ? + FOR UPDATE + ", [DB::getPdo()->lastInsertId()])[0]; + } + + if (Carbon::now()->gte("2024-01-22")) { + $prefix_number = $numbering["prefix"]; + $next_number = $numbering["current"] + 1; + $pad_number = $numbering["pad"] - strlen($prefix_number); + $number = $prefix_number . str_pad($next_number, $pad_number, 0, STR_PAD_LEFT); + DB::statement("UPDATE numbering SET current = current+1 WHERE id = ?", [$numbering["id"]]); + } else { + $count = DB::select("select last_value from pos_invoices_id_seq")[0]->last_value; + $number = "POS" . str_pad($count + 1, 6, 0, STR_PAD_LEFT); + } + + + // $count = DB::select("select last_value from pos_invoices_id_seq")[0]->last_value; + $data["user_id"] = auth()->user()->id; + $data["voucher"] = (float) @$data["voucher"]; + $data["time"] = Carbon::now(); + $data["vouchers"] = @$data["vouchers"] ? implode(",", @$data["vouchers"]):""; + $data["number"] = $number; + $subtotal = (float) @$data["subtotal"]; + $discount = (float) @$data["discount"]; + $invoice = PosInvoice::create($data); + + $exclude_item_points = []; + + $i = 0; + foreach ($data["details"] as $row) { + $i++; + + $item = Items::find($row["item_id"]); + $row["line_no"] = $i; + $row["unit_price"] = (float) @$row["unit_price"]; + $row["total"] = (float) @$row["total"]; + $row["discount"] = (float) @$row["discount"]; + $row["vat"] = (float) @$row["vat"]; + $row["unit_cost"] = @$item ? $item->unit_cost : 0; + $row["line_discount"] = @$row["discount"] * @$row["qty"]; + $row["invoice_discount"] = $subtotal == 0 ? 0: ($row["total"] / $subtotal) * $discount; + $row["net_price"] = @$row['net_price']; + + if (@$row['serial_number']){ + $this->activateSerialNumber($invoice, @$row['serial_number'],[ + "reference" => @$row["reference"], + "description" => @$row["description"], + "item_number" => @$row["item_number"], + "variant_code" => @$row["variant_code"], + "brand" => @$item->dimension->brand + ]); + } + $row["pricelist_discount"] = @$row['pricelist_discount']; + if (strpos($item->name, "VOUCHER") > -1) { + $exp = $row["unit_price"] == 0 ? 3 : 6; + $row["reference"] = $this->issueVoucher($invoice, $item->name, $row["reference"], (float) @$row["qty"], $exp); + } + $invoice->details()->create($row); + } + + // Handle DISCOUNT vouchers - save to pos_invoice_vouchers table + if (@$data["discount_vouchers"]) { + $this->useDiscountVoucher($invoice, $data["discount_vouchers"]); + } + + // Handle SALE vouchers (payment vouchers) - legacy behavior + if (@$data["vouchers"]) + $exclude_item_points = $this->useVoucher($invoice, $data["vouchers"]); + + $this->checkPoint($invoice, $exclude_item_points); + + foreach ($data["payments"] as $row) { + + if ($row["amount"] <= 0) + continue; + + $invoice->payments()->create($row); + + if ($row['method'] == "POINT"){ + + DB::table("customer_points") + ->insert([ + "description" => "Use points for payment transaction " . $invoice->number, + "point" => ($row["amount"] / 1000) * -1, + "customer_id" => $invoice->customer_id, + "reference_id" => $invoice->id, + "reference_type" => get_class($invoice), + "created_at" => Carbon::now() + ]); + }else if ($row['method'] == "VOUCHER"){ + + $voucher = Voucher::where('number', $row['card_number'])->first(); + if ($voucher){ + // Validate voucher is SALE type for payment method + if (($voucher->calculation_type ?? 'SALE') != 'SALE') { + throw ValidationException::withMessages([ + "voucher" => "Voucher ini adalah Voucher Discount, bukan Voucher Belanja. Tidak bisa digunakan sebagai pembayaran." + ]); + } + + $voucher->used_at = Carbon::now(); + $voucher->reference_used_id = $invoice->id; + $voucher->reference_used_type = get_class($invoice); + $voucher->save(); + } + } + } + + $luckyWheel = LuckyWheel::where("valid_at","<=", Carbon::now()) + ->where("expired_at",">=", Carbon::now()) + ->where(function($query) use ($location){ + $query->whereNull("location_id") + ->orWhere("location_id", @$location->id); + }) + ->where("min_sales_total","<", $invoice->total) + ->first(); + + if ($luckyWheel){ + $ticket_counts = $luckyWheel->tickets()->count(); + if ($ticket_counts < $luckyWheel->max_ticket || $luckyWheel->max_ticket == -1){ + $max_prize = $luckyWheel->max_prize; + $current_prize = $luckyWheel->gets()->whereNotNull("redeem_at")->sum("nominal"); + +// if ($max_prize > $current_prize){ + + // $ticket2 = $luckyWheel->tickets()->where("customer_id", $invoice->customer_id)->first(); + // if ($ticket2 == null){ + $ticket = $luckyWheel->tickets()->create([ + "invoice_id" => $invoice->id, + "customer_id" => $invoice->customer_id, + "max_times" => 10 + ]); + // } + // } + } + } + + $survey = Survey::where("name","AFTER PURCHASE")->first(); + if ($survey){ + $data = [ + "channel" => "pos", + "customer_id" => $invoice->customer_id, + "invoice_id" => $invoice->id + ]; + $feedback = $this->surveyRepository->generateFeedback($survey, $data); + if ($feedback){ + $notif = new SurveyBroadcast($feedback); + $invoice->customer->notify($notif); + } + } + + return $invoice; + }); + } + + public function deactivateSerialNumber($invoice, $sn){ + if (!@$sn) + return; + + $arr = explode(",",$sn); + SerialNumberDetail::whereIn("number",$arr)->update([ + "activated_at" => null + ]); + SerialNumberLog::whereIn('number',$arr)->delete(); + } + + public function activateSerialNumber($invoice, $sn, $data){ + if (!@$sn) + return; + + $user = auth()->user(); + $arr = explode(",",$sn); + foreach($arr as $row){ + $snDetail = SerialNumberDetail::where("number",$row)->first(); + if ($snDetail){ + $snDetail->activated_at = Carbon::now(); + $snDetail->activated_by = $user->id; + $snDetail->invoice_id = $invoice->id; + $snDetail->save(); + } + + $snLog = new SerialNumberLog; + $snLog->number = $row; + $snLog->activated_at = Carbon::now(); + $snLog->reference_number = $data["reference"] ?? ""; + $snLog->item_number = $data["item_number"] ?? ""; + $snLog->variant_code = $data["variant_code"] ?? ""; + $snLog->description = $data["description"] ?? ""; + $snLog->brand = $data["brand"] ?? ""; + $snLog->invoice_no = $invoice->number; + $snLog->save(); + } + } + + public function checkPoint($invoice, $item_ids) + { + + $point = 0; + foreach ($invoice->details as $detail) { + $discount_items = DB::select("select apply_point from discounts left join discount_items on discounts.id = discount_items.discount_id + where item_reference_id = ? and valid_at <= NOW() and expired_at >= NOW() order by discounts.id desc", [@$detail->item_reference_id]); + + $apply_point = count($discount_items) > 0 ? $discount_items[0]->apply_point : false; + $discount = (float) @$detail->discount; + $qty = (float) @$detail->qty; + + if(!$apply_point) { + + if ($discount > 0){ + continue; + } + + if ($detail->unit_price < $detail->item->net_price){ + continue; + } + + // skip is has discount from voucher + if (in_array($detail->item_reference_id, $item_ids)){ + continue; + } + } + + $rows = DB::select( + "select point from customer_point_rules WHERE item_reference_id = ? AND qty = ?", + [$detail->item_reference_id, $detail->qty] + ); + if (count($rows)) { + + $row_point = (float) @$rows[0]->point; + $detail->point = $row_point * $qty; + $detail->save(); + $point += $detail->point; + } else { + + $rows_general = DB::select( + "select point from customer_point_rules WHERE item_reference_id = ? AND (qty is null or qty = 0)", + [$detail->item_reference_id] + ); + + if (count($rows_general)) { + $row_point = (float) @$rows_general[0]->point; + $detail->point = $row_point * $qty; + $detail->save(); + $point += $detail->point; + } + } + } + + if ($point > 0 && $invoice->location->code != "IND") { + DB::table("customer_points") + ->insert([ + "description" => "Get points from transaction " . $invoice->number, + "point" => $point, + "customer_id" => $invoice->customer_id, + "reference_id" => $invoice->id, + "reference_type" => get_class($invoice), + "created_at" => Carbon::now() + ]); + } + } + + private function useVoucher($invoice, $vouchers) + { + $exclude_item_points = []; + $arr = explode(",", $vouchers); + foreach ($arr as $voucher) { + $item = Voucher::where("number", $voucher)->first(); + if (!$item) + continue; + + $item_id = $this->checkAffiliatorVoucher($invoice, $item); + if (count($item_id) > 0) + { + $exclude_item_points = array_merge($exclude_item_points, $item_id); + } + + if (@$item->event){ + $item_reference_ids = $item->event->items->pluck("id")->toArray(); + $exclude_item_points = array_merge($exclude_item_points, $item_reference_ids); + } + + $item->used_at = Carbon::now(); + $item->reference_used_id = $invoice->id; + $item->reference_used_type = get_class($invoice); + $item->save(); + } + + return $exclude_item_points; + } + + private function useDiscountVoucher($invoice, $discount_vouchers) + { + // discount_vouchers is an array of voucher codes + foreach ($discount_vouchers as $voucher_code) { + $voucher = Voucher::where("number", $voucher_code)->first(); + if (!$voucher) + continue; + + // Validate it's a DISCOUNT type voucher + if (($voucher->calculation_type ?? 'SALE') != 'DISCOUNT') { + throw ValidationException::withMessages([ + "discount_voucher" => "Voucher {$voucher_code} bukan Voucher Discount." + ]); + } + + // Create pos_invoice_vouchers record + \App\Models\PosInvoiceVoucher::create([ + 'pos_invoice_id' => $invoice->id, + 'voucher_id' => $voucher->id, + 'nominal' => $voucher->nominal + ]); + + // Mark voucher as used + $voucher->used_at = Carbon::now(); + $voucher->reference_used_id = $invoice->id; + $voucher->reference_used_type = get_class($invoice); + $voucher->save(); + } + } + + private function findDetail($invoice, $item_reference_ids){ + return $invoice->details->filter(function($detail) use ($item_reference_ids){ + return in_array($detail->item_reference_id , $item_reference_ids); + })->reduce(function($acc, $detail){ + return $acc + ($detail->unit_price - $detail->unit_cost); + },0); + } + + public function checkAffiliatorVoucher($invoice, $voucher){ + if ($voucher->reference_issued_type != VoucherClaim::class){ + return []; + } + $issued = $voucher->referenceIssued; + + if ($issued->claimable_type != AffiliatorItemCode::class){ + return []; + } + + $claimable = $issued->claimable; + if ($claimable == null){ + return []; + } + + $affiliator = $claimable->affiliator; + + if ($claimable->affiliatorItem != null){ + $affiliatorItem = $claimable->affiliatorItem; + + $itemReference = $affiliatorItem->item; + $item_reference_ids = [$itemReference->id]; + $gross = $this->findDetail($invoice, $item_reference_ids); + $fee_nominal = $affiliatorItem->fee ? $affiliatorItem->fee : $gross * ($affiliator->fee_percentage/100); + + }else if ($claimable->codeable_type == Affiliatoritem::class){ + + $affiliatorItem = $claimable->codeable; + $itemReference = $affiliatorItem->item; + $item_reference_ids = [$itemReference->id]; + $gross = $this->findDetail($invoice, $item_reference_ids); + $fee_nominal = $affiliatorItem->fee ? $affiliatorItem->fee : $gross * ($affiliator->fee_percentage/100); + + }else if ($claimable->codeable_type == VoucherEvent::class){ + $voucherEvent = $claimable->codeable; + $item_reference_ids = $voucherEvent->items->pluck("id")->toArray(); + $gross = $this->findDetail($invoice, $item_reference_ids); + $fee_nominal = $gross * ($affiliator->fee_percentage/100); + }else{ + return []; + } + + + $incentive = new Incentive; + $incentive->fill([ + "employee_id" => 0, + "person_id" => $affiliator->id, + "person_type" => get_class($affiliator), + "nominal" => $fee_nominal, + "date" => Carbon::now(), + "reference_id" => $voucher->id, + "reference_type" => get_class($voucher), + "status" => "OPEN" + ]); + $incentive->save(); + + return $item_reference_ids; + } + + private function generateVoucher($nominal) + { + $code = "BLJ"; + if ($nominal == 2000000) { + $code = "2JT"; + } else if ($nominal == 1000000) { + $code = "1JT"; + } else if ($nominal == 1500000) { + $code = "1.5JT"; + } else if ($nominal == 2500000) { + $code = "2.5JT"; + } else if ($nominal == 500000) { + $code = "500RB"; + } else if ($nominal == 750000) { + $code = "750RB"; + } else if ($nominal == 250000) { + $code = "250RB"; + } else if ($nominal == 100000) { + $code = "100RB"; + } else if ($nominal == 50000) { + $code = "50RB"; + } else if ($nominal == 3000000) { + $code = "3JT"; + } + $voucher = null; + $iter = 0; + while ($voucher == null) { + $new_code = strtoupper("EV/" . $code . "/" . date("Y") . "/" . bin2hex(openssl_random_pseudo_bytes(3))); + $exists = Voucher::where("number", $new_code)->first(); + $voucher = $exists ? null : $new_code; + } + return $voucher; + } + + private function issueVoucher($invoice, $name, $vouchers, $qty, $exp) + { + + $nominal = str_replace(".", "", $name); + preg_match("/[0-9]++/", $nominal, $match); + $nominal = (float) @$match[0]; + + $evoucher = true; + if ($vouchers == "") { + $arr = []; + for ($i = 0; $i < $qty; $i++) { + $arr[] = $this->generateVoucher($nominal); + } + + $evoucher = true; + } else { + $arr = explode(",", $vouchers); + + $evoucher = false; + } + foreach ($arr as $voucher) { + $voucher = trim($voucher); + $item = Voucher::where("number", $voucher)->firstOrNew(); + $item->customer_id = $invoice->customer_id; + $item->evoucher = $evoucher; + $item->number = $voucher; + $item->nominal = $nominal; + $item->issued_at = Carbon::now(); + $item->expired_at = Carbon::now()->addMonth($exp); + $item->reference_issued_id = $invoice->id; + $item->reference_issued_type = get_class($invoice); + $item->save(); + } + + return implode(",", $arr); + } + + public function update(PosInvoice $invoice, array $data) + { + if (!$this->isAdmin()) { + $isCreatedToday = Carbon::now()->diffInMinutes(Carbon::parse($invoice->time)) <= (60 * 24); + if (!$isCreatedToday) { + return abort(403, 'hanya invoice yang belum 24 jam yang bisa di edit'); + } + } + + $data["vouchers"] = implode(",", $data["vouchers"]); + + $invoice->update($data); + + $ids = []; + foreach ($data["details"] as $row) { + $detail = $invoice->details()->find(@$row["id"]); + if (!$detail){ + $item = Items::find($row["item_id"]); + $row["unit_price"] = (float) @$row["unit_price"]; + $row["total"] = (float) @$row["total"]; + $row["unit_cost"] = @$item ? $item->unit_cost : 0; + $detail = $invoice->details()->create($row); + }else{ + $detail->update([ + "qty" => $row["qty"], + "serial_number" => $row["serial_number"], + "total" => $row["total"], + "unit_price" => $row["unit_price"], + "discount" => $row["discount"], + ]); + $item = $detail->item; + } + + $this->deactivateSerialNumber($invoice, @$row['serial_number']); + + $this->activateSerialNumber($invoice, @$row['serial_number'],[ + "reference" => @$row["reference"], + "description" => @$row["description"], + "item_number" => @$row["item_number"], + "variant_code" => @$row["variant_code"], + "brand" => @$item->dimension->brand + ]); + + $ids[] = $detail->id; + } + $invoice->details()->whereNotIn("id",$ids)->delete($ids); + + $ids = []; + foreach ($data["payments"] as $row) { + if ($row["amount"] <= 0) + continue; + + $detail = $invoice->details()->find(@$row["id"]); + if (!$detail){ + $detail = $invoice->payments()->create($row); + }else{ + $detail->update($row); + } + $ids[] = $detail->id; + } + $invoice->payments()->whereNotIn("id",$ids)->delete(); + + return $invoice; + } + + public function cancel(PosInvoice $item, $data) + { + $data["canceled_by"] = auth()->user()->id; + $data["canceled_at"] = Carbon::now(); + $item->fill($data); + $item->save(); + + DB::table("customer_points")->where("reference_id", $item->id) + ->where("reference_type", get_class($item))->delete(); + + DB::table("vouchers") + ->where("reference_used_id", $item->id) + ->where("reference_used_type", get_class($item)) + ->update([ + "used_at" => null + ]); + } + + // public function delete(PosInvoice $item) + // { + // $item->delete(); + // } + + public function findBy($column, $value) + { + $item = PosInvoice::where($column, $value)->firstOrFail(); + return $item; + } + + public function isAdmin() + { + $admin = Role::where('name', 'ADMIN')->first(); + $user = auth()->user(); + + return $user->role_id == $admin->id; + } +} diff --git a/app/ThirdParty/Biteship/Biteship.php b/app/ThirdParty/Biteship/Biteship.php new file mode 100644 index 0000000..054f5b1 --- /dev/null +++ b/app/ThirdParty/Biteship/Biteship.php @@ -0,0 +1,40 @@ +rate = new Rate; + $this->order = new Order; + $this->tracking = new Tracking; + } + + public function trackingByWaybill($params){ + return $this->tracking->byWaybill($params); + } + + public function rateByPostal($params){ + return $this->rate->byPostal($params); + } + + public function rateByLatlong($params){ + return $this->rate->byLatLong($params); + } + + public function orderByPostal($params){ + return $this->order->byPostal($params); + } + + public function orderByLatLong($params){ + return $this->order->byLatLong($params); + } + + public function trackingById($params){ + return $this->tracking->byId($params); + } +} diff --git a/app/ThirdParty/Biteship/Order.php b/app/ThirdParty/Biteship/Order.php new file mode 100644 index 0000000..7b88974 --- /dev/null +++ b/app/ThirdParty/Biteship/Order.php @@ -0,0 +1,116 @@ + $key + ]) + ->withBody(json_encode([ + "origin_contact_name" => $params["origin_contact_name"], + "origin_contact_phone" => $params["origin_contact_phone"], + "origin_address" => $params["origin_address"], + "origin_postal_code" => $params["origin_postal_code"], + "origin_coordinate" => [ + "latitude" => $params["origin_latitude"], + "longitude" => $params["origin_longitude"], + ], + + "destination_contact_name" => $params["destination_contact_name"], + "destination_contact_phone" => $params["destination_contact_phone"], + "destination_address" => $params["destination_address"], + "destination_postal_code" => $params["destination_postal_code"], + + "reference_id" => $params["reference_id"], + "courier_insurance" => $params["courier_insurance"], + + "courier_company" => $params["courier_company"], + "courier_type" => $params["courier_type"], + "delivery_type" => "now", + "items" => $params["items"] + + ]), 'application/json') + ->post($url."/v1/orders"); + + if ($res->status() == 200) + return $res->json(); + else{ + Log::error("Biteship order error", $res->json()); + throw new Exception($res->json()['error']); + } + + return null; + } + + public function byLatLong($params){ + + $url = env("BITESHIP_URL"); + $key = env("BITESHIP_KEY"); + $res = Http::withHeaders([ + "authorization" => $key + ]) + ->withBody(json_encode([ + "origin_contact_name" => $params["origin_contact_name"], + "origin_contact_phone" => $params["origin_contact_phone"], + "origin_address" => $params["origin_address"], + "origin_coordinate" => [ + "latitude" => $params["origin_latitude"], + "longitude" => $params["origin_longitude"], + ], + + "destination_contact_name" => $params["destination_contact_name"], + "destination_contact_phone" => $params["destination_contact_phone"], + "destination_address" => $params["destination_address"], + "destination_coordinate" => [ + "latitude" => $params["destination_latitude"], + "longitude" => $params["destination_longitude"], + ], + + "courier_company" => $params["courier_company"], + "courier_type" => $params["courier_type"], + "delivery_type" => "now", + "items" => $params["items"] + + ]), 'application/json') + ->post($url."/v1/orders"); + + if ($res->status() == 200) + return $res->json(); + else{ + Log::error("Biteship order error", [$res->json()]); + throw new Exception($res->json()['error']); + } + + return null; + + } + + + public function confirm($id){ + + $url = env("BITESHIP_URL"); + $key = env("BITESHIP_KEY"); + $res = Http::withHeaders([ + "authorization" => $key + ]) + ->post($url."/v1/orders/".$id."/confirm"); + + if ($res->status() == 200) + return $res->json(); + else + Log::error("Biteship order error", [$res->json()]); + + return null; + } + +} diff --git a/app/ThirdParty/Biteship/Rate.php b/app/ThirdParty/Biteship/Rate.php new file mode 100644 index 0000000..7e72ec2 --- /dev/null +++ b/app/ThirdParty/Biteship/Rate.php @@ -0,0 +1,81 @@ + $key + ]) + ->withBody(json_encode([ + "origin_latitude" => $origin_latitude, + "origin_longitude" => $origin_longitude, + "destination_latitude" => $destination_latitude, + "destination_longitude" => $destination_longitude, + "couriers" => env("BITESHIP_COURIER_ALL","grab,gojek,tiki,jnt,anteraja"), + "items" => $items + ]), 'application/json') + ->post($url."/v1/rates/couriers"); + + if ($res->status() == 200) + return $res->json(); + + return null; + }); + } + + public function byPostal($params) + { + $destination_postal_code = $params["destination_postal_code"]; + $origin_postal_code = $params["origin_postal_code"]; + $items = $params["items"]; + $sha1 = sha1(json_encode($items)); + + $key = $origin_postal_code."_".$destination_postal_code."_".$sha1; + return Cache::remember("rates_".$key, 60 * 60 * 24, function() use ( + $origin_postal_code, + $destination_postal_code, + $items) { + $url = env("BITESHIP_URL"); + $key = env("BITESHIP_KEY"); + $res = Http::withHeaders([ + "authorization" => $key + ]) + ->withBody(json_encode([ + "origin_postal_code" => $origin_postal_code, + "destination_postal_code" => $destination_postal_code, + "couriers" => env("BITESHIP_COURIER","tiki,jnt,anteraja"), + "items" => $items + ]), 'application/json') + ->post($url."/v1/rates/couriers"); + + if ($res->status() == 200) + return $res->json(); + + return null; + }); + } +} diff --git a/app/ThirdParty/Biteship/Tracking.php b/app/ThirdParty/Biteship/Tracking.php new file mode 100644 index 0000000..027c5c7 --- /dev/null +++ b/app/ThirdParty/Biteship/Tracking.php @@ -0,0 +1,54 @@ + $key + ]) + ->get($url."/v1/trackings/".$waybill_id."/couriers/".$courier); + + if ($res->status() == 200) + return $res->json(); + + return null; + }); + } + + + public function byId($params) + { + $id = $params["id"]; + + $key = $id ; + return Cache::remember("tracking_id_".$key, 60 * 60 * 24, function() use ($id) { + $url = env("BITESHIP_URL"); + $key = env("BITESHIP_KEY"); + $res = Http::withHeaders([ + "authorization" => $key + ]) + ->get($url."/v1/trackings/".$id); + + if ($res->status() == 200) + return $res->json(); + + return null; + }); + } + +} diff --git a/lang/en/cart_summary.php b/lang/en/cart_summary.php new file mode 100644 index 0000000..75ba172 --- /dev/null +++ b/lang/en/cart_summary.php @@ -0,0 +1,15 @@ + 'Order summary', + 'edit' => 'Edit', + 'subtotal' => 'Subtotal', + 'items_count' => ':count items', + 'savings' => 'Saving', + 'tax_collected' => 'Tax collected', + 'shipping' => 'Shipping', + 'calculated_at_checkout' => 'Calculated at checkout', + 'estimated_total' => 'Estimated total', + 'bonuses_earned' => 'Congratulations! You have earned :count bonuses', + 'create_account' => 'Create an account', + 'and_get' => 'and get', +]; \ No newline at end of file diff --git a/lang/en/checkout.php b/lang/en/checkout.php new file mode 100644 index 0000000..c3cfea7 --- /dev/null +++ b/lang/en/checkout.php @@ -0,0 +1,47 @@ + 'Delivery Method', + 'choose_delivery_method' => 'Choose how you want to receive your order', + 'delivery' => 'Delivery', + 'delivery_description' => 'We deliver to your address', + 'store_pickup' => 'Store Pickup', + 'pickup_description' => 'Pick up from our store', + 'postcode' => 'Postcode', + 'postcode_placeholder' => 'e.g. 12345', + 'calculate_shipping' => 'Calculate Shipping', + 'calculating' => 'Calculating...', + 'shipping_address' => 'Shipping Address', + 'first_name' => 'First Name', + 'last_name' => 'Last Name', + 'address' => 'Address', + 'address2' => 'Address Line 2', + 'optional' => 'optional', + 'city' => 'City', + 'state' => 'State', + 'select_state' => 'Select State', + 'zip' => 'ZIP Code', + 'phone' => 'Phone', + 'select_store' => 'Select Store', + 'asia_golf_jakarta' => 'Asia Golf Jakarta', + 'asia_golf_bandung' => 'Asia Golf Bandung', + 'store_address' => 'Address', + 'hours' => 'Hours', + 'continue_to_payment' => 'Continue to Payment', + 'free' => 'FREE', + 'shipping' => 'Shipping', + 'enter_postcode' => 'Please enter your postcode', + 'calculate_shipping_first' => 'Please calculate shipping first', + 'fill_required_fields' => 'Please fill in all required fields', + 'select_store' => 'Please select a store', + 'select_saved_address' => 'Select Saved Address', + 'choose_address' => 'Choose an address', + 'add_new_address' => 'Add New Address', + 'primary' => 'Primary', + 'continue_to_shipping' => 'Continue to Shipping', + 'choose_shipping' => 'Choose Shipping', + 'payment' => 'Payment', + 'delivery' => 'Delivery', + 'pickup' => 'Pickup', + 'pickup_ready' => 'Your order will be ready for pickup at the selected store', + 'continue_to_payment' => 'Continue to Payment', +]; diff --git a/lang/id/cart_summary.php b/lang/id/cart_summary.php new file mode 100644 index 0000000..41b63a5 --- /dev/null +++ b/lang/id/cart_summary.php @@ -0,0 +1,15 @@ + 'Ringkasan Pesanan', + 'edit' => 'Edit', + 'subtotal' => 'Subtotal', + 'items_count' => ':count barang', + 'savings' => 'Hemat', + 'tax_collected' => 'Pajak yang dipungut', + 'shipping' => 'Pengiriman', + 'calculated_at_checkout' => 'Dihitung saat checkout', + 'estimated_total' => 'Total perkiraan', + 'bonuses_earned' => 'Selamat! Anda telah mendapatkan :count bonus', + 'create_account' => 'Buat akun', + 'and_get' => 'dan dapatkan', +]; \ No newline at end of file diff --git a/lang/id/checkout.php b/lang/id/checkout.php new file mode 100644 index 0000000..744cbae --- /dev/null +++ b/lang/id/checkout.php @@ -0,0 +1,36 @@ + 'Metode Pengiriman', + 'choose_delivery_method' => 'Pilih bagaimana Anda ingin menerima pesanan Anda', + 'delivery' => 'Pengiriman', + 'delivery_description' => 'Kami mengirim ke alamat Anda', + 'store_pickup' => 'Ambil di Toko', + 'pickup_description' => 'Ambil dari toko kami', + 'postcode' => 'Kode Pos', + 'postcode_placeholder' => 'contoh: 12345', + 'calculate_shipping' => 'Hitung Ongkir', + 'calculating' => 'Menghitung...', + 'shipping_address' => 'Alamat Pengiriman', + 'first_name' => 'Nama Depan', + 'last_name' => 'Nama Belakang', + 'address' => 'Alamat', + 'address2' => 'Alamat Baris 2', + 'optional' => 'opsional', + 'city' => 'Kota', + 'state' => 'Provinsi', + 'select_state' => 'Pilih Provinsi', + 'zip' => 'Kode Pos', + 'phone' => 'Telepon', + 'select_store' => 'Pilih Toko', + 'asia_golf_jakarta' => 'Asia Golf Jakarta', + 'asia_golf_bandung' => 'Asia Golf Bandung', + 'store_address' => 'Alamat', + 'hours' => 'Jam', + 'continue_to_payment' => 'Lanjut ke Pembayaran', + 'free' => 'GRATIS', + 'shipping' => 'Pengiriman', + 'enter_postcode' => 'Silakan masukkan kode pos Anda', + 'calculate_shipping_first' => 'Silakan hitung ongkir terlebih dahulu', + 'fill_required_fields' => 'Silakan isi semua field yang wajib diisi', + 'select_store' => 'Silakan pilih toko', +]; diff --git a/resources/views/checkout/v1-cart.blade.php b/resources/views/checkout/v1-cart.blade.php index 8011e6f..baa7593 100644 --- a/resources/views/checkout/v1-cart.blade.php +++ b/resources/views/checkout/v1-cart.blade.php @@ -202,7 +202,7 @@ $0.00 + href="{{ route('checkout.delivery') }}"> Proceed to checkout diff --git a/resources/views/checkout/v1-delivery-1-shipping.blade.php b/resources/views/checkout/v1-delivery-1-shipping.blade.php new file mode 100644 index 0000000..c2d2428 --- /dev/null +++ b/resources/views/checkout/v1-delivery-1-shipping.blade.php @@ -0,0 +1,460 @@ +@extends('layouts.landing', ['title' => 'Checkout v.1 - Delivery Info Step 1']) + +@section('content') + + + +
+
+
+ + +
+
+
+
1
+
+

{{ __('checkout.delivery_method') }}

+ + @if ($delivery_method == 'shipping') +

{{ __('checkout.delivery') }}

+ @if ($address) +

{{ $address->location }}

+ @endif + @else +

{{ __('checkout.pickup') }}

+ @endif +
+
+
+
2
+ + +
+

{{ __('checkout.choose_shipping') }} +

+
+ @csrf + + + + @if ($delivery_method == 'shipping') + @foreach ($shipping_list as $shipping) +
+ first ? 'checked' : '' }}> + +
+ @endforeach + @else +
+ + {{ __('checkout.pickup_ready') }} +
+ @endif + + +
+
+
+
+
3
+

{{ __('checkout.payment') }}

+
+
+
+ + + + +
+
+
+ + @include('layouts.partials/footer') +@endsection + +@section('scripts') + +@endsection diff --git a/resources/views/checkout/v1-delivery-1.blade.php b/resources/views/checkout/v1-delivery-1.blade.php index 14b5e08..43f554f 100644 --- a/resources/views/checkout/v1-delivery-1.blade.php +++ b/resources/views/checkout/v1-delivery-1.blade.php @@ -1,70 +1,7 @@ @extends('layouts.landing', ['title' => 'Checkout v.1 - Delivery Info Step 1']) @section('content') - -
-
-

Your order

- -
-
- - -
- - iPhone 14 - -
-
- Apple iPhone 14 128GB White -
-
$899.00
-
Qty: 1
-
-
- - -
- - -10% - iPad Pro - -
-
- Tablet Apple iPad Pro M2 -
-
$989.00 $1,099.00
-
Qty: 1
-
-
- - -
- - Smart Watch - -
-
- Smart Watch Series 7, White -
-
$429.00
-
Qty: 1
-
-
-
- -
- Edit cart -
-
- - @include('layouts.partials/offcanvas') - - @include('layouts.partials/navbar', ['wishlist' => true]) +
@@ -78,33 +15,176 @@
1
-

Delivery information

+

{{ __('checkout.delivery_method') }}

-

Add your Postcode to see the delivery and collection options - available in your area.

-
-
- - +

{{ __('checkout.choose_delivery_method') }}

+ + +
+
+
+
+ + +
+
+
+
+ + +
+
- - Calculate cost and availability - - +
+ + +
+ + {{-- dropdown address --}} + @if ($address_list->count() > 0) +
+ + +
+ @endif + + +
+
{{ __('checkout.shipping_address') }}
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+
+ + + + + +
+
+ @csrf + + + +
+
-
+
2
-

Shipping address

+

{{ __('checkout.choose_shipping') }} +

3
-

Payment

+

{{ __('checkout.payment') }}

@@ -113,74 +193,8 @@ @@ -188,8 +202,364 @@
@include('layouts.partials/footer') - @endsection @section('scripts') + @endsection diff --git a/resources/views/components/checkout/order-summary.blade.php b/resources/views/components/checkout/order-summary.blade.php new file mode 100644 index 0000000..ce6f1c4 --- /dev/null +++ b/resources/views/components/checkout/order-summary.blade.php @@ -0,0 +1,67 @@ +@props([ + 'subtotal' => 0, + 'total' => 0, + 'savings' => 0, + 'tax' => 0, + 'showEdit' => false, + 'editUrl' => null, + 'showItems' => false, + 'items' => [], +]) + +
+
+
+
+
{{ __('cart_summary.title') }}
+ @if ($showEdit && $editUrl) + + @endif +
+ @if ($showItems && count($items) > 0) + + @foreach ($items->take(3) as $item) +
+ {{ $item->name ?? 'Product' }} +
+ @endforeach + +
+ @endif +
+
    +
  • +
    {{ __('cart_summary.subtotal') }} ( + {{ __('cart_summary.items_count', ['count' => auth()->check() ? \App\Repositories\Member\Cart\MemberCartRepository::getCount() : 0]) }}): +
    + Rp + {{ number_format($subtotal, 0, ',', '.') }} +
  • +
  • + {{ __('cart_summary.savings') }}: + Rp {{ number_format($savings, 0, ',', '.') }} +
  • + +
  • + {{ __('cart_summary.shipping') }}: + {{ __('cart_summary.calculated_at_checkout') }} +
  • +
+
+
+ {{ __('cart_summary.estimated_total') }}: + Rp {{ number_format($total, 0, ',', '.') }} +
+ +
+
+
diff --git a/routes/web.php b/routes/web.php index f04442e..7549c81 100644 --- a/routes/web.php +++ b/routes/web.php @@ -14,6 +14,7 @@ use App\Http\Controllers\SearchController; use App\Http\Controllers\ComponentController; use App\Http\Controllers\Auth\ProfileController; use App\Http\Controllers\CartController; +use App\Http\Controllers\CheckoutController; Route::group(['prefix' => '/dummy'], function () { Route::get('', [RoutingController::class, 'index'])->name('root'); @@ -94,4 +95,16 @@ Route::middleware(['auth'])->prefix('/cart')->group(function () { Route::delete('/clear', [CartController::class, 'clear'])->name('cart.clear'); Route::delete('/{id}', [CartController::class, 'delete'])->name('cart.delete'); Route::get('/count', [CartController::class, 'count'])->name('cart.count'); +}); + +Route::middleware(['auth'])->prefix('/checkout')->group(function () { + + + Route::get('/', [CheckoutController::class, 'index'])->name('checkout.delivery'); + Route::post('/', [CheckoutController::class, 'indexProcess'])->name('checkout.delivery.process'); + + Route::get('/shipping', [CheckoutController::class, 'chooseShipping'])->name('checkout.shipping'); + Route::post('/shipping', [CheckoutController::class, 'chooseShippingProcess'])->name('checkout.shipping.process'); + Route::get('/payment', [CheckoutController::class, 'choosePayment'])->name('checkout.payment'); + }); \ No newline at end of file