checkout page, fill address, choose shipping, calculation

This commit is contained in:
Bayu Lukman Yusuf 2026-01-27 21:41:25 +07:00
parent 9a23e75f5b
commit d298649398
51 changed files with 5212 additions and 149 deletions

View File

@ -0,0 +1,159 @@
<?php
namespace App\Http\Controllers;
use App\Models\Address;
use App\Models\Location;
use App\Repositories\Member\Cart\MemberCartRepository;
use Illuminate\Http\Request;
class CheckoutController extends Controller
{
public function index(Request $request, MemberCartRepository $memberCartRepository)
{
$request->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');
}
}
}

View File

@ -0,0 +1,31 @@
<?php
namespace App\Http\Requests\Member\Transaction;
use Illuminate\Foundation\Http\FormRequest;
class CancelRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*
* @return bool
*/
public function authorize()
{
$transaction = $this->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',
];
}
}

View File

@ -0,0 +1,31 @@
<?php
namespace App\Http\Requests\Member\Transaction;
use Illuminate\Foundation\Http\FormRequest;
class CloseRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*
* @return bool
*/
public function authorize()
{
$transaction = $this->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',
];
}
}

View File

@ -0,0 +1,30 @@
<?php
namespace App\Http\Requests\Member\Transaction;
use Illuminate\Foundation\Http\FormRequest;
class DeliverRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*
* @return bool
*/
public function authorize()
{
$transaction = $this->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',
];
}
}

View File

@ -0,0 +1,31 @@
<?php
namespace App\Http\Requests\Member\Transaction;
use Illuminate\Foundation\Http\FormRequest;
class DetailRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*
* @return bool
*/
public function authorize()
{
$transaction = $this->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',
];
}
}

View File

@ -0,0 +1,30 @@
<?php
namespace App\Http\Requests\Member\Transaction;
use Illuminate\Foundation\Http\FormRequest;
class ProcessRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*
* @return bool
*/
public function authorize()
{
$transaction = $this->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',
];
}
}

View File

@ -0,0 +1,34 @@
<?php
namespace App\Http\Requests\Member\Transaction;
use Illuminate\Foundation\Http\FormRequest;
class TransactionRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*
* @return bool
*/
public function authorize()
{
return true;
}
public function rules()
{
return [
'address_id' => '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',
];
}
}

23
app/Models/LuckyWheel.php Normal file
View File

@ -0,0 +1,23 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class LuckyWheel extends Model
{
use HasFactory;
public function prizes(){
return $this->hasMany(LuckyWheelPrize::class);
}
public function gets(){
return $this->hasMany(LuckyWheelGet::class);
}
public function tickets(){
return $this->hasMany(LuckyWheelTicket::class);
}
}

View File

@ -0,0 +1,21 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class LuckyWheelGet extends Model
{
use HasFactory;
protected $fillable = ['prize_id','lucky_wheel_id','voucher_id','nominal','redeem_at'];
public function prize(){
return $this->belongsTo(LuckyWheelPrize::class,'prize_id');
}
public function voucher(){
return $this->belongsTo(Voucher::class,'voucher_id');
}
}

View File

@ -0,0 +1,28 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Carbon\Carbon;
class LuckyWheelPrize extends Model
{
use HasFactory;
protected $fillable = ['name','voucher_event_id'];
public function voucher(){
return $this->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());
}
}

View File

@ -0,0 +1,38 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class LuckyWheelTicket extends Model
{
use HasFactory;
protected $fillable = ['customer_id','invoice_id','max_times'];
public function luckyWheel()
{
return $this->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");
}
}

82
app/Models/PosInvoice.php Normal file
View File

@ -0,0 +1,82 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Spatie\Activitylog\Traits\LogsActivity;
use Spatie\Activitylog\LogOptions;
class PosInvoice extends Model
{
use HasFactory;
use LogsActivity;
public function getActivitylogOptions(): LogOptions
{
return LogOptions::defaults();
}
protected $fillable = ["location_id","customer_id","sales_id","sales2_id",
"user_id","time","number",
"note","subtotal", "voucher", "discount","tax","total",
"vouchers","canceled_at","canceled_note","canceled_by", "note_internal"];
public function ticket()
{
return $this->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");
}
}

View File

@ -0,0 +1,35 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class PosInvoiceDetail extends Model
{
use HasFactory;
protected $fillable = ['item_reference_id','point', 'invoice_id', 'item_id','item_variant_id','note','qty','unit_price','unit','reference','discount','vat','total','unit_cost','line_no',
'description','item_number','variant_code','invoice_discount', 'line_discount', 'net_price', 'pricelist_discount','serial_number'
];
public function itemReference()
{
return $this->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');
}
}

View File

@ -0,0 +1,18 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class PosInvoicePayment extends Model
{
use HasFactory;
protected $fillable = ['method', 'amount', 'bank', 'card_number', 'remarks', 'installment', 'bank_id'];
public function bank_raw()
{
return $this->belongsTo(Bank::class, 'bank_id', 'id');
}
}

View File

@ -0,0 +1,29 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class PosInvoiceVoucher extends Model
{
use HasFactory;
protected $table = 'pos_invoice_vouchers';
protected $fillable = [
'pos_invoice_id',
'voucher_id',
'nominal'
];
public function invoice()
{
return $this->belongsTo(PosInvoice::class, 'pos_invoice_id');
}
public function voucher()
{
return $this->belongsTo(Voucher::class, 'voucher_id');
}
}

70
app/Models/Sales.php Normal file
View File

@ -0,0 +1,70 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Builder;
use Cviebrock\EloquentSluggable\Sluggable;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Spatie\Activitylog\Traits\LogsActivity;
use Spatie\Activitylog\LogOptions;
class Sales extends Model
{
use HasFactory, SoftDeletes, Sluggable;
use LogsActivity;
public function getActivitylogOptions(): LogOptions
{
return LogOptions::defaults();
}
protected $table = 'sales';
protected $primaryKey = 'id';
protected $fillable = [
'id',
'number',
'name',
'phone',
'email',
'address',
'country',
'province_id',
'city_id',
'district_id',
'village_id',
'postal_code',
'customer_group_id',
'location_id',
];
public function sluggable(): array
{
return [
'number' => [
'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');
}
}

View File

@ -0,0 +1,25 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
class SerialNumber extends Model
{
use HasFactory, SoftDeletes;
protected $table = 'sn_batches';
protected $fillable = ['item_reference_id', 'reference_number', 'item_number', 'variant_code',
'description', 'user_id', 'qty','status','closed_at'];
public function details() {
return $this->hasMany(SerialNumberDetail::class, 'sn_batch_id', 'id');
}
public function user() {
return $this->belongsTo(User::class);
}
}

View File

@ -0,0 +1,27 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class SerialNumberDetail extends Model
{
use HasFactory;
protected $table = 'sn_batch_details';
protected $fillable = ['sn_batch_id', 'number','activated_at','invoice_id','activated_by'];
public function data(){
return $this->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");
}
}

View File

@ -0,0 +1,14 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
class SerialNumberLog extends Model
{
use HasFactory, SoftDeletes;
protected $fillable = ['reference_number','item_number','variant_code','description','brand','activated_at','invoice_no'];
}

24
app/Models/Survey.php Normal file
View File

@ -0,0 +1,24 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
class Survey extends Model
{
use HasFactory, SoftDeletes;
protected $table = 'survey';
protected $fillable = ['code', 'name', 'title', 'content', 'voucher_event_id'];
public function detail() {
return $this->hasMany(SurveyQuestion::class);
}
public function voucherEvent() {
return $this->belongsTo(VoucherEvent::class);
}
}

View File

@ -0,0 +1,29 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class SurveyFeedback extends Model
{
use HasFactory;
protected $fillable = ['code','survey_id', 'channel', 'time', 'customer_id', 'invoice_id', 'ip_address', 'agent'];
public function detail() {
return $this->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);
}
}

View File

@ -0,0 +1,13 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class SurveyFeedbackDetail extends Model
{
use HasFactory;
protected $fillable = ['survey_question_id', 'survey_feedback_id', 'value', 'description'];
}

View File

@ -0,0 +1,16 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
class SurveyQuestion extends Model
{
use HasFactory, SoftDeletes;
protected $fillable = ['survey_id', 'type', 'data', 'description', 'order'];
public $timestamps = false;
}

34
app/Models/XenditLink.php Normal file
View File

@ -0,0 +1,34 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class XenditLink extends Model
{
use HasFactory;
protected $fillable = [
"uid",
"invoice_url",
"user_id",
"external_id",
"status",
"amount",
"received_amount",
"expiry_date",
"payment_id",
"paid_amount",
"payment_method",
"bank_code",
"payment_channel",
"payment_destination",
"paid_at"
];
public function payment()
{
return $this->morphOne(TransactionPayment::class,"method");
}
}

View File

@ -0,0 +1,57 @@
<?php
namespace App\Notifications;
use Illuminate\Notifications\Notification;
use Kreait\Firebase\Messaging\CloudMessage;
use App\Models\User;
use App\Models\UserDevice;
class FcmChannel
{
/**
* Send the given notification.
*/
public function send(object $notifiable, Notification $notification): void
{
// $payload = $notification->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);
// }
}
}

View File

@ -0,0 +1,83 @@
<?php
namespace App\Notifications\Member\Transaction;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Notifications\Notification;
use App\Models\Transaction;
use App\Notifications\FcmChannel;
class NewOrder extends Notification implements ShouldQueue
{
use Queueable;
protected $transaction;
/**
* Create a new notification instance.
*
* @return void
*/
public function __construct(Transaction $transaction)
{
$this->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"],
]
];
}
}

View File

@ -0,0 +1,84 @@
<?php
namespace App\Notifications\Member\Transaction;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Notifications\Notification;
use App\Models\Transaction;
use App\Notifications\FcmChannel;
class OrderCanceled extends Notification implements ShouldQueue
{
use Queueable;
protected $transaction;
/**
* Create a new notification instance.
*
* @return void
*/
public function __construct(Transaction $transaction)
{
$this->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"],
]
];
}
}

View File

@ -0,0 +1,83 @@
<?php
namespace App\Notifications\Member\Transaction;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Notifications\Notification;
use App\Models\Transaction;
use App\Notifications\FcmChannel;
class OrderDelivered extends Notification implements ShouldQueue
{
use Queueable;
protected $transaction;
/**
* Create a new notification instance.
*
* @return void
*/
public function __construct(Transaction $transaction)
{
$this->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"],
]
];
}
}

View File

@ -0,0 +1,83 @@
<?php
namespace App\Notifications\Member\Transaction;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Notifications\Notification;
use App\Models\Transaction;
use App\Notifications\FcmChannel;
class OrderOnDelivery extends Notification implements ShouldQueue
{
use Queueable;
protected $transaction;
/**
* Create a new notification instance.
*
* @return void
*/
public function __construct(Transaction $transaction)
{
$this->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"],
]
];
}
}

View File

@ -0,0 +1,83 @@
<?php
namespace App\Notifications\Member\Transaction;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Notifications\Notification;
use App\Models\Transaction;
use App\Notifications\FcmChannel;
class OrderPaid extends Notification implements ShouldQueue
{
use Queueable;
protected $transaction;
/**
* Create a new notification instance.
*
* @return void
*/
public function __construct(Transaction $transaction)
{
$this->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"],
]
];
}
}

View File

@ -0,0 +1,83 @@
<?php
namespace App\Notifications\Member\Transaction;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Notifications\Notification;
use App\Models\Transaction;
use App\Notifications\FcmChannel;
class OrderProcessed extends Notification implements ShouldQueue
{
use Queueable;
protected $transaction;
/**
* Create a new notification instance.
*
* @return void
*/
public function __construct(Transaction $transaction)
{
$this->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"],
]
];
}
}

View File

@ -0,0 +1,95 @@
<?php
namespace App\Notifications\Member\Transaction;
use App\Models\Transaction;
use App\Notifications\FcmChannel;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Notifications\Notification;
class OrderWaitPayment extends Notification implements ShouldQueue
{
use Queueable;
protected $transaction;
/**
* Create a new notification instance.
*
* @return void
*/
public function __construct(Transaction $transaction)
{
$this->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'],
],
];
}
}

View File

@ -0,0 +1,219 @@
<?php
namespace App\Repositories\Crm;
use App\Models\Survey;
use App\Models\SurveyFeedback;
use App\Models\SurveyFeedbackDetail;
use App\Models\SurveyQuestion;
use App\Notifications\Crm\SurveyRespond;
use App\Repositories\Member\VoucherEvent\VoucherEventRepository;
use Carbon\Carbon;
use Illuminate\Support\Str;
use DB;
class SurveyRepository
{
var $voucherEventRepository;
public function __construct(
VoucherEventRepository $voucherEventRepository) {
$this->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;
}
}

View File

@ -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);

View File

@ -0,0 +1,255 @@
<?php
namespace App\Repositories\Member;
use App\ThirdParty\Biteship\Biteship;
use App\Models\Cart;
use App\Models\Location;
use App\Models\Address;
use App\Models\Transaction;
use Illuminate\Support\Facades\DB;
use Illuminate\Validation\ValidationException;
use Carbon\Carbon;
class ShippingRepository
{
var $biteship;
public function __construct(Biteship $biteship) {
$this->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();
}
}
}

View File

@ -0,0 +1,26 @@
<?php
namespace App\Http\Controllers\Member\Transaction;
use App\Http\Controllers\Controller;
use App\Http\Requests\Member\Transaction\TransactionRequest;
use App\Http\Resources\Member\Transaction\CheckoutResource;
use App\Repositories\Member\Transaction\TransactionRepository;
use App\Notifications\Member\Transaction\OrderWaitPayment;
use Illuminate\Support\Facades\Notification;
class CheckoutController extends Controller
{
public function index(TransactionRequest $request, TransactionRepository $repository)
{
$data = $request->validated();
$item = $repository->create($data);
$notification = new OrderWaitPayment($item);
$user = auth()->user();
$user->notify($notification->delay(now()->addMinutes(1)));
return new CheckoutResource($item);
}
}

View File

@ -0,0 +1,872 @@
<?php
namespace App\Repositories\Member\Transaction;
use App\Repositories\Member\Voucher\VoucherRepository;
use App\Models\Transaction;
use App\Models\Customer;
use App\Models\TransactionDetail;
use App\Models\TransactionStatus;
use App\Models\TransactionPayment;
use App\Models\XenditLink;
use App\Models\Location;
use App\Models\Items;
use App\Models\ItemReference;
use App\Models\Voucher;
use App\Models\Cart;
use Illuminate\Support\Facades\DB;
use App\Helpers\AutoNumbering;
use App\Models\CustomerPoint;
use App\Repositories\Member\ShippingRepository;
use App\Repositories\Pos\InvoiceRepository;
use Illuminate\Validation\ValidationException;
use App\ThirdParty\Xendit\Xendit;
use App\Models\TransactionShipping;
use Carbon\Carbon;
use Exception;
use App\Repositories\Auth\RoleRepository;
use Illuminate\Support\Facades\Notification;
use App\Notifications\Member\Transaction\NewOrder;
use App\Notifications\Member\Transaction\OrderPaid;
use App\Notifications\Member\Transaction\OrderProcessed;
use App\Notifications\Member\Transaction\OrderOnDelivery;
use App\Notifications\Member\Transaction\OrderDelivered;
use App\Notifications\Member\Transaction\OrderCanceled;
class TransactionRepository
{
var $shippingRepository;
var $invoiceRepository;
var $roleRepository;
var $voucherRepository;
var $xendit;
public function __construct(ShippingRepository $shippingRepository,
InvoiceRepository $invoiceRepository,
VoucherRepository $voucherRepository,
RoleRepository $roleRepository,
Xendit $xendit) {
$this->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;
}
}

View File

@ -0,0 +1,842 @@
<?php
namespace App\Repositories\Pos;
use App\Models\Bank;
use App\Models\Survey;
use App\Models\PosInvoice;
use Illuminate\Support\Facades\DB;
use Carbon\Carbon;
use App\Models\Items;
use App\Models\Location;
use App\Models\Role;
use App\Models\Sales;
use App\Models\Voucher;
use App\Models\VoucherClaim;
use App\Models\AffiliatorItem;
use App\Models\VoucherEvent;
use App\Models\AffiliatorItemCode;
use App\Models\Incentive;
use App\Helpers\AutoNumbering;
use App\Models\XenditLink;
use App\Models\ItemReference;
use App\Models\LuckyWheel;
use App\Models\SerialNumberDetail;
use App\Models\SerialNumberLog;
use App\Notifications\Crm\SurveyBroadcast;
use App\Repositories\Crm\SurveyRepository;
use Illuminate\Validation\ValidationException;
class InvoiceRepository
{
var $surveyRepository;
public function __construct(SurveyRepository $surveyRepository){
$this->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;
}
}

40
app/ThirdParty/Biteship/Biteship.php vendored Normal file
View File

@ -0,0 +1,40 @@
<?php
namespace App\ThirdParty\Biteship;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Cache;
class Biteship
{
public function __construct() {
$this->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);
}
}

116
app/ThirdParty/Biteship/Order.php vendored Normal file
View File

@ -0,0 +1,116 @@
<?php
namespace App\ThirdParty\Biteship;
use Exception;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Cache;
class Order
{
public function byPostal($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_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;
}
}

81
app/ThirdParty/Biteship/Rate.php vendored Normal file
View File

@ -0,0 +1,81 @@
<?php
namespace App\ThirdParty\Biteship;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Cache;
class Rate
{
public function byLatLong($params)
{
$origin_latitude = $params["origin_latitude"];
$origin_longitude = $params["origin_longitude"];
$destination_latitude = $params["destination_latitude"];
$destination_longitude = $params["destination_longitude"];
$items = $params["items"];
$sha1 = sha1(json_encode($items));
$key = implode("_", [$origin_latitude, $origin_longitude, $destination_latitude, $destination_longitude, $sha1]);
return Cache::remember("rates_".$key, 60 * 60 * 24, function()
use ($origin_latitude,
$origin_longitude,
$destination_latitude,
$destination_longitude,
$items) {
$url = env("BITESHIP_URL");
$key = env("BITESHIP_KEY");
$res = Http::withHeaders([
"authorization" => $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;
});
}
}

54
app/ThirdParty/Biteship/Tracking.php vendored Normal file
View File

@ -0,0 +1,54 @@
<?php
namespace App\ThirdParty\Biteship;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Cache;
class Tracking
{
public function byWaybill($params)
{
$waybill_id = $params["waybill_id"];
$courier = $params["courier"];
$key = $waybill_id ." ". $courier;
return Cache::remember("tracking_waybill_".$key, 60 * 60 * 24, function() use ($waybill_id, $courier) {
$url = env("BITESHIP_URL");
$key = env("BITESHIP_KEY");
$res = Http::withHeaders([
"authorization" => $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;
});
}
}

15
lang/en/cart_summary.php Normal file
View File

@ -0,0 +1,15 @@
<?php
return [
'title' => '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',
];

47
lang/en/checkout.php Normal file
View File

@ -0,0 +1,47 @@
<?php
return [
'delivery_method' => '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',
];

15
lang/id/cart_summary.php Normal file
View File

@ -0,0 +1,15 @@
<?php
return [
'title' => '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',
];

36
lang/id/checkout.php Normal file
View File

@ -0,0 +1,36 @@
<?php
return [
'delivery_method' => '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',
];

View File

@ -202,7 +202,7 @@
<span class="h5 mb-0" id="cart-estimated-total">$0.00</span>
</div>
<a class="btn btn-lg btn-primary w-100"
href="{{ route('second', ['checkout', 'v1-delivery-1']) }}">
href="{{ route('checkout.delivery') }}">
Proceed to checkout
<i class="ci-chevron-right fs-lg ms-1 me-n1"></i>
</a>

View File

@ -0,0 +1,460 @@
@extends('layouts.landing', ['title' => 'Checkout v.1 - Delivery Info Step 1'])
@section('content')
<x-layout.header />
<!-- Page content -->
<main class="content-wrapper">
<div class="container py-5">
<div class="row pt-1 pt-sm-3 pt-lg-4 pb-2 pb-md-3 pb-lg-4 pb-xl-5">
<!-- Delivery info (Step 1) -->
<div class="col-lg-8 col-xl-7 mb-5 mb-lg-0">
<div class="d-flex flex-column gap-5 pe-lg-4 pe-xl-0">
<div class="d-flex align-items-start">
<div class="d-flex align-items-center justify-content-center bg-body-secondary text-body-secondary rounded-circle fs-sm fw-semibold lh-1 flex-shrink-0"
style="width: 2rem; height: 2rem; margin-top: -.125rem">1</div>
<div class="w-100 ps-3 ps-md-4">
<h2 class="h5 text-body-secondary mb-md-4">{{ __('checkout.delivery_method') }}</h2>
@if ($delivery_method == 'shipping')
<p>{{ __('checkout.delivery') }}</p>
@if ($address)
<p>{{ $address->location }}</p>
@endif
@else
<p>{{ __('checkout.pickup') }}</p>
@endif
</div>
</div>
<div class="d-flex align-items-start" id="shippingAddressStep">
<div class="d-flex align-items-center justify-content-center bg-primary text-white rounded-circle fs-sm fw-semibold lh-1 flex-shrink-0"
style="width: 2rem; height: 2rem; margin-top: -.125rem">2</div>
<div class="w-100 ps-3 ps-md-4">
<h1 class="h5 mb-12">{{ __('checkout.choose_shipping') }}
</h1>
<form action="{{ route('checkout.shipping.process') }}" method="post">
@csrf
<input type="hidden" name="delivery_method" value="{{ $delivery_method }}">
<input type="hidden" name="address_id" value="{{ $address_id }}">
@if ($delivery_method == 'shipping')
@foreach ($shipping_list as $shipping)
<div class="form-check mb-3">
<input class="form-check-input" type="radio" name="shipping_option"
id="shipping_{{ $loop->index }}"
value="{{ $shipping['courier'] }}|{{ $shipping['service'] }}|{{ $shipping['cost'] }}"
{{ $loop->first ? 'checked' : '' }}>
<label
class="form-check-label d-flex justify-content-between align-items-center"
for="shipping_{{ $loop->index }}">
<div>
<strong>{{ $shipping['title'] }}</strong>
<div class="text-muted small">{{ $shipping['courier'] }} -
{{ $shipping['service'] }}</div>
</div>
<div class="text-primary fw-bold">
Rp {{ number_format($shipping['cost'], 0, ',', '.') }}
</div>
</label>
</div>
@endforeach
@else
<div class="alert alert-info">
<i class="ci-store me-2"></i>
{{ __('checkout.pickup_ready') }}
</div>
@endif
<button type="submit" class="btn btn-lg btn-primary w-100 mt-4">
<span>{{ __('checkout.continue_to_payment') }}</span>
<i class="ci-chevron-right fs-lg ms-1 me-n1"></i>
</button>
</form>
</div>
</div>
<div class="d-flex align-items-start">
<div class="d-flex align-items-center justify-content-center bg-body-secondary text-body-secondary rounded-circle fs-sm fw-semibold lh-1 flex-shrink-0"
style="width: 2rem; height: 2rem; margin-top: -.125rem">3</div>
<h2 class="h5 text-body-secondary ps-3 ps-md-4 mb-0">{{ __('checkout.payment') }}</h2>
</div>
</div>
</div>
<!-- Order summary (sticky sidebar) -->
<aside class="col-lg-4 offset-xl-1" style="margin-top: -100px">
<div class="position-sticky top-0" style="padding-top: 100px">
<x-checkout.order-summary :subtotal="$subtotal" :total="$total" :savings="0" :tax="0"
:showEdit="true" :editUrl="route('second', ['checkout', 'v1-cart'])" />
</div>
</aside>
</div>
</div>
</main>
@include('layouts.partials/footer')
@endsection
@section('scripts')
<script>
document.addEventListener('DOMContentLoaded', function() {
// Delivery method selection
const deliveryOption = document.getElementById('deliveryOption');
const pickupOption = document.getElementById('pickupOption');
const deliveryOptions = document.getElementById('deliveryOptions');
const pickupOptions = document.getElementById('pickupOptions');
const shippingAddress = document.getElementById('shippingAddress');
const continueButton = document.getElementById('continueButton');
// Handle delivery method change
deliveryOption.addEventListener('change', function() {
if (this.checked) {
deliveryOptions.style.display = 'block';
pickupOptions.style.display = 'none';
resetPickupSelection();
// Update button text for delivery
document.getElementById('continueButtonText').textContent =
'{{ __('checkout.continue_to_shipping') }}';
// Update hidden input values
document.getElementById('deliveryMethodInput').value = 'delivery';
// Update address ID from selected address
const addressSelect = document.getElementById('addressSelect');
if (addressSelect && addressSelect.value) {
document.getElementById('addressIdInput').value = addressSelect.value;
}
// Show shipping address step and shipping row
const shippingAddressStep = document.getElementById('shippingAddressStep');
const shippingRow = document.getElementById('shipping-row');
if (shippingAddressStep) {
console.log("Showing shipping address step");
shippingAddressStep.style.visibility = 'visible';
shippingAddressStep.style.height = 'auto';
shippingAddressStep.style.overflow = 'visible';
shippingAddressStep.style.margin = '';
shippingAddressStep.style.padding = '';
}
if (shippingRow) {
console.log("Showing shipping row");
shippingRow.style.visibility = 'visible';
shippingRow.style.height = 'auto';
shippingRow.style.overflow = 'visible';
shippingRow.style.margin = '';
shippingRow.style.padding = '';
} else {
console.log("Shipping row not found");
}
}
});
pickupOption.addEventListener('change', function() {
if (this.checked) {
deliveryOptions.style.display = 'none';
pickupOptions.style.display = 'block';
resetDeliverySelection();
// Update button text for pickup
document.getElementById('continueButtonText').textContent =
'{{ __('checkout.continue_to_payment') }}';
// Update hidden input values
document.getElementById('deliveryMethodInput').value = 'pickup';
document.getElementById('addressIdInput').value = '';
// Hide shipping address step and shipping row
const shippingAddressStep = document.getElementById('shippingAddressStep');
const shippingRow = document.getElementById('shipping-row');
console.log("Elements found:", shippingAddressStep, shippingRow);
if (shippingAddressStep) {
console.log("Hiding shipping address step");
shippingAddressStep.style.visibility = 'hidden';
shippingAddressStep.style.height = '0';
shippingAddressStep.style.overflow = 'hidden';
shippingAddressStep.style.margin = '0';
shippingAddressStep.style.padding = '0';
}
if (shippingRow) {
console.log("Hiding shipping row");
shippingRow.style.visibility = 'hidden';
shippingRow.style.height = '0';
shippingRow.style.overflow = 'hidden';
shippingRow.style.margin = '0';
shippingRow.style.padding = '0';
} else {
console.log("Shipping row not found");
}
}
});
// Address selection handler
const addressSelect = document.getElementById('addressSelect');
if (addressSelect) {
// Auto-populate form with first selected address on page load
function populateAddressForm() {
const selectedOption = addressSelect.options[addressSelect.selectedIndex];
if (selectedOption && selectedOption.value) {
// Populate all shipping address form fields
document.getElementById('firstName').value = selectedOption.dataset.firstName || '';
document.getElementById('lastName').value = selectedOption.dataset.lastName || '';
document.getElementById('address').value = selectedOption.dataset.address || '';
document.getElementById('city').value = selectedOption.dataset.city || '';
document.getElementById('state').value = selectedOption.dataset.state || '';
document.getElementById('zip').value = selectedOption.dataset.postcode || '';
document.getElementById('phone').value = selectedOption.dataset.phone || '';
document.getElementById('postcode').value = selectedOption.dataset.postcode || '';
// Update order summary with postcode
if (selectedOption.dataset.postcode) {
updateOrderSummaryWithShipping();
}
}
}
// Populate on page load
populateAddressForm();
// Set initial address ID
if (addressSelect.value) {
document.getElementById('addressIdInput').value = addressSelect.value;
}
addressSelect.addEventListener('change', function() {
const selectedOption = this.options[this.selectedIndex];
if (this.value === 'new' || this.value === '') {
// Clear form fields for new address
document.getElementById('firstName').value = '';
document.getElementById('lastName').value = '';
document.getElementById('address').value = '';
document.getElementById('city').value = '';
document.getElementById('state').value = '';
document.getElementById('zip').value = '';
document.getElementById('phone').value = '';
document.getElementById('postcode').value = '';
// Clear address ID input
document.getElementById('addressIdInput').value = '';
} else {
// Populate form fields with selected address
document.getElementById('firstName').value = selectedOption.dataset.firstName || '';
document.getElementById('lastName').value = selectedOption.dataset.lastName || '';
document.getElementById('address').value = selectedOption.dataset.address || '';
document.getElementById('city').value = selectedOption.dataset.city || '';
document.getElementById('state').value = selectedOption.dataset.state || '';
document.getElementById('zip').value = selectedOption.dataset.postcode || '';
document.getElementById('phone').value = selectedOption.dataset.phone || '';
document.getElementById('postcode').value = selectedOption.dataset.postcode || '';
// Update address ID input
document.getElementById('addressIdInput').value = this.value;
// Update order summary with postcode
if (selectedOption.dataset.postcode) {
updateOrderSummaryWithShipping();
}
}
});
}
// Auto-update order summary when postcode changes
document.getElementById('postcode').addEventListener('input', function() {
if (this.value) {
updateOrderSummaryWithShipping();
}
});
// Store selection for pickup
const storeRadios = document.querySelectorAll('input[name="store"]');
storeRadios.forEach(radio => {
radio.addEventListener('change', function() {
if (this.checked) {
updateOrderSummaryForPickup();
}
});
});
// Form validation before continue
continueButton.addEventListener('click', function(e) {
const selectedMethod = document.querySelector('input[name="deliveryMethod"]:checked').value;
if (selectedMethod === 'delivery') {
if (!validateDeliveryForm()) {
e.preventDefault();
return;
}
} else if (selectedMethod === 'pickup') {
if (!validatePickupForm()) {
e.preventDefault();
return;
}
}
});
function resetDeliverySelection() {
resetShippingCalculation();
}
function resetPickupSelection() {
document.querySelectorAll('input[name="store"]').forEach(radio => {
radio.checked = false;
});
resetShippingCalculation();
}
function resetShippingCalculation() {
// Reset order summary to original state
const shippingElement = document.querySelector('[data-shipping-cost]');
if (shippingElement) {
shippingElement.style.display = 'none';
}
const totalElement = document.getElementById('cart-estimated-total');
if (totalElement && window.originalTotal) {
totalElement.textContent = `Rp ${window.originalTotal}`;
}
}
function updateOrderSummaryWithShipping() {
// Simulate shipping cost calculation based on postcode
const shippingCost = calculateShippingCost(document.getElementById('postcode').value);
// Update order summary
const subtotalElement = document.getElementById('cart-subtotal');
const currentSubtotal = parseFloat(subtotalElement.textContent.replace(/[^\d]/g, ''));
const newTotal = currentSubtotal + shippingCost;
// Store original total
if (!window.originalTotal) {
window.originalTotal = subtotalElement.textContent;
}
// Update total
const totalElement = document.getElementById('cart-estimated-total');
totalElement.textContent = `Rp ${number_format(newTotal, 0, ',', '.')}`;
// Show shipping cost in summary
showShippingCost(shippingCost);
}
function updateOrderSummaryForPickup() {
// Pickup is usually free
const subtotalElement = document.getElementById('cart-subtotal');
const currentSubtotal = parseFloat(subtotalElement.textContent.replace(/[^\d]/g, ''));
// Store original total
if (!window.originalTotal) {
window.originalTotal = subtotalElement.textContent;
}
// Update total (same as subtotal for pickup)
const totalElement = document.getElementById('cart-estimated-total');
totalElement.textContent = subtotalElement.textContent;
// Show free shipping
showShippingCost(0, true);
}
function calculateShippingCost(postcode) {
// Simple shipping cost calculation based on postcode
// In real implementation, this would call an API
const jakartaPostcodes = ['10000', '10110', '10220', '10310', '10410'];
const bandungPostcodes = ['40111', '40112', '40113', '40114', '40115'];
if (jakartaPostcodes.includes(postcode)) {
return 15000; // Jakarta: Rp 15,000
} else if (bandungPostcodes.includes(postcode)) {
return 25000; // Bandung: Rp 25,000
} else {
return 35000; // Other areas: Rp 35,000
}
}
function showShippingCost(cost, isFree = false) {
// Find or create shipping cost element in order summary
let shippingElement = document.querySelector('[data-shipping-cost]');
if (!shippingElement) {
const orderSummary = document.querySelector('.list-unstyled');
const shippingLi = document.createElement('li');
shippingLi.className = 'd-flex justify-content-between';
shippingLi.setAttribute('data-shipping-cost', '');
shippingLi.innerHTML = `
<span>{{ __('checkout.shipping') }}:</span>
<span class="text-dark-emphasis fw-medium" id="shipping-cost">Rp 0</span>
`;
orderSummary.appendChild(shippingLi);
shippingElement = shippingLi;
}
const costElement = document.getElementById('shipping-cost');
if (isFree) {
costElement.textContent = '{{ __('checkout.free') }}';
costElement.className = 'text-success fw-medium';
} else {
costElement.textContent = `Rp ${number_format(cost, 0, ',', '.')}`;
costElement.className = 'text-dark-emphasis fw-medium';
}
shippingElement.style.display = 'flex';
}
function validateDeliveryForm() {
const postcode = document.getElementById('postcode').value;
const firstName = document.getElementById('firstName').value;
const address = document.getElementById('address').value;
const city = document.getElementById('city').value;
const phone = document.getElementById('phone').value;
if (!postcode) {
alert('{{ __('checkout.enter_postcode') }}');
return false;
}
if (!firstName || !address || !city || !phone) {
alert('{{ __('checkout.fill_required_fields') }}');
return false;
}
return true;
}
function validatePickupForm() {
const selectedStore = document.querySelector('input[name="store"]:checked');
if (!selectedStore) {
alert('{{ __('checkout.select_store') }}');
return false;
}
return true;
}
// Number formatting helper
function number_format(number, decimals, dec_point, thousands_sep) {
number = (number + '').replace(/[^0-9+\-Ee.]/g, '');
var n = !isFinite(+number) ? 0 : +number;
var prec = !isFinite(+decimals) ? 0 : Math.abs(decimals);
var sep = (typeof thousands_sep === 'undefined') ? ',' : thousands_sep;
var dec = (typeof dec_point === 'undefined') ? '.' : dec_point;
var s = '';
var toFixedFix = function(n, prec) {
var k = Math.pow(10, prec);
return '' + Math.round(n * k) / k;
};
s = (prec ? toFixedFix(n, prec) : '' + Math.round(n)).split('.');
if (thousands_sep) {
var re = /(-?\d+)(\d{3})/;
while (re.test(s[0])) {
s[0] = s[0].replace(re, '$1' + sep + '$2');
}
}
if ((s[1] || '').length < prec) {
s[1] = s[1] || '';
s[1] += new Array(prec - s[1].length + 1).join('0');
}
return s.join(dec);
}
});
</script>
@endsection

View File

@ -1,70 +1,7 @@
@extends('layouts.landing', ['title' => 'Checkout v.1 - Delivery Info Step 1'])
@section('content')
<!-- Order preview offcanvas -->
<div class="offcanvas offcanvas-end pb-sm-2 px-sm-2" id="orderPreview" tabindex="-1" aria-labelledby="orderPreviewLabel"
style="width: 500px">
<div class="offcanvas-header py-3 pt-lg-4">
<h4 class="offcanvas-title" id="orderPreviewLabel">Your order</h4>
<button type="button" class="btn-close" data-bs-dismiss="offcanvas" aria-label="Close"></button>
</div>
<div class="offcanvas-body d-flex flex-column gap-3 py-2">
<!-- Item -->
<div class="d-flex align-items-center">
<a class="flex-shrink-0" href="{{ route('second', ['shop', 'product-general-electronics']) }}">
<img src="/img/shop/electronics/thumbs/08.png" width="110" alt="iPhone 14">
</a>
<div class="w-100 min-w-0 ps-2 ps-sm-3">
<h5 class="d-flex animate-underline mb-2">
<a class="d-block fs-sm fw-medium text-truncate animate-target"
href="{{ route('second', ['shop', 'product-general-electronics']) }}">Apple iPhone 14 128GB White</a>
</h5>
<div class="h6 mb-0">$899.00</div>
<div class="fs-xs pt-2">Qty: 1</div>
</div>
</div>
<!-- Item -->
<div class="d-flex align-items-center">
<a class="position-relative flex-shrink-0" href="{{ route('second', ['shop', 'product-general-electronics']) }}">
<span class="badge text-bg-danger position-absolute top-0 start-0">-10%</span>
<img src="/img/shop/electronics/thumbs/09.png" width="110" alt="iPad Pro">
</a>
<div class="w-100 min-w-0 ps-2 ps-sm-3">
<h5 class="d-flex animate-underline mb-2">
<a class="d-block fs-sm fw-medium text-truncate animate-target"
href="{{ route('second', ['shop', 'product-general-electronics']) }}">Tablet Apple iPad Pro M2</a>
</h5>
<div class="h6 mb-0">$989.00 <del class="text-body-tertiary fs-xs fw-normal">$1,099.00</del></div>
<div class="fs-xs pt-2">Qty: 1</div>
</div>
</div>
<!-- Item -->
<div class="d-flex align-items-center">
<a class="flex-shrink-0" href="{{ route('second', ['shop', 'product-general-electronics']) }}">
<img src="/img/shop/electronics/thumbs/01.png" width="110" alt="Smart Watch">
</a>
<div class="w-100 min-w-0 ps-2 ps-sm-3">
<h5 class="d-flex animate-underline mb-2">
<a class="d-block fs-sm fw-medium text-truncate animate-target"
href="{{ route('second', ['shop', 'product-general-electronics']) }}">Smart Watch Series 7, White</a>
</h5>
<div class="h6 mb-0">$429.00</div>
<div class="fs-xs pt-2">Qty: 1</div>
</div>
</div>
</div>
<div class="offcanvas-header">
<a class="btn btn-lg btn-outline-secondary w-100" href="checkout-v1-cart']) }}">Edit cart</a>
</div>
</div>
@include('layouts.partials/offcanvas')
@include('layouts.partials/navbar', ['wishlist' => true])
<x-layout.header />
<!-- Page content -->
<main class="content-wrapper">
@ -78,33 +15,176 @@
<div class="d-flex align-items-center justify-content-center bg-primary text-white rounded-circle fs-sm fw-semibold lh-1 flex-shrink-0"
style="width: 2rem; height: 2rem; margin-top: -.125rem">1</div>
<div class="w-100 ps-3 ps-md-4">
<h1 class="h5 mb-md-4">Delivery information</h1>
<h1 class="h5 mb-md-4">{{ __('checkout.delivery_method') }}</h1>
<div class="ms-n5 ms-sm-0">
<p class="fs-sm mb-md-4">Add your Postcode to see the delivery and collection options
available in your area.</p>
<div class="d-flex flex-column flex-md-row align-items-md-end gap-3 gap-xl-4">
<div class="w-100">
<label for="postcode" class="form-label">Postcode</label>
<input type="text" class="form-control form-control-lg" id="postcode"
placeholder="e.g. H1 1AG">
<p class="fs-sm mb-md-4">{{ __('checkout.choose_delivery_method') }}</p>
<!-- Delivery Method Selection -->
<div class="delivery-method-selection mb-4">
<div class="row">
<div class="col-md-6">
<div class="form-check">
<input class="form-check-input" type="radio" name="deliveryMethod"
id="deliveryOption" value="delivery" checked>
<label class="form-check-label d-flex align-items-center"
for="deliveryOption">
<div>
<div class="fw-medium">
{{ __('checkout.delivery') }}</div>
<div class="text-muted small">
{{ __('checkout.delivery_description') }}</div>
</div>
<a class="btn btn-lg btn-primary" href="{{ route('second', ['checkout', 'v1-delivery-2']) }}">
Calculate cost and availability
</label>
</div>
</div>
<div class="col-md-6">
<div class="form-check">
<input class="form-check-input" type="radio" name="deliveryMethod"
id="pickupOption" value="pickup">
<label class="form-check-label d-flex align-items-center"
for="pickupOption">
<div>
<div class="fw-medium">{{ __('checkout.store_pickup') }}</div>
<div class="text-muted small">
{{ __('checkout.pickup_description') }}</div>
</div>
</label>
</div>
</div>
</div>
</div>
<!-- Delivery Options -->
<div id="deliveryOptions" class="delivery-options">
{{-- dropdown address --}}
@if ($address_list->count() > 0)
<div class="mb-4">
<label for="addressSelect"
class="form-label">{{ __('checkout.select_saved_address') }}</label>
<select class="form-select form-select-lg" id="addressSelect">
@foreach ($address_list as $key => $address)
<option value="{{ $address->id }}"
data-first-name="{{ $address->name }}" data-last-name=""
data-address="{{ $address->address }}"
data-city="{{ $address->city_name }}"
data-state="{{ $address->province_name }}"
data-postcode="{{ $address->postal_code }}"
data-phone="{{ $address->phone }}"
{{ $address->is_primary || $key === 0 ? 'selected' : '' }}>
{{ $address->label }} - {{ $address->name }},
{{ $address->address }}, {{ $address->city_name }},
{{ $address->province_name }} {{ $address->postal_code }}
@if ($address->is_primary)
<span
class="badge bg-primary ms-2">{{ __('checkout.primary') }}</span>
@endif
</option>
@endforeach
</select>
</div>
@endif
<!-- Shipping Address (shown automatically) -->
<div id="shippingAddress" class="shipping-address mt-4" style="display: block;">
<h6 class="mb-3">{{ __('checkout.shipping_address') }}</h6>
<div class="row g-3">
<div class="col-md-6">
<label for="firstName"
class="form-label">{{ __('checkout.first_name') }}</label>
<input type="text" class="form-control" id="firstName" readonly
style="border: none; background-color: #f8f9fa;">
</div>
<div class="col-md-6">
<label for="lastName"
class="form-label">{{ __('checkout.last_name') }}</label>
<input type="text" class="form-control" id="lastName" readonly
style="border: none; background-color: #f8f9fa;">
</div>
<div class="col-12">
<label for="address"
class="form-label">{{ __('checkout.address') }}</label>
<input type="text" class="form-control" id="address" readonly
style="border: none; background-color: #f8f9fa;">
</div>
<div class="col-md-6">
<label for="city"
class="form-label">{{ __('checkout.city') }}</label>
<input type="text" class="form-control" id="city" readonly
style="border: none; background-color: #f8f9fa;">
</div>
<div class="col-md-4">
<label for="state"
class="form-label">{{ __('checkout.state') }}</label>
<input type="text" class="form-control" id="state" readonly
style="border: none; background-color: #f8f9fa;">
</div>
<div class="col-md-2">
<label for="zip"
class="form-label">{{ __('checkout.zip') }}</label>
<input type="text" class="form-control" id="zip" readonly
style="border: none; background-color: #f8f9fa;">
</div>
<div class="col-12">
<label for="phone"
class="form-label">{{ __('checkout.phone') }}</label>
<input type="tel" class="form-control" id="phone" readonly
style="border: none; background-color: #f8f9fa;">
</div>
</div>
</div>
</div>
<!-- Pickup Options -->
<div id="pickupOptions" class="pickup-options" style="display: none;">
<div class="store-list">
<div class="form-check mb-3">
<input type="hidden" name="store" value="{{ $store->id }}">
<label class="form-check-label" style="margin-left: 0px;" for="store1">
<div class="fw-medium">{{ $store->display_name }}</div>
<div class="text-muted small">
{{ __('checkout.store_address') }}: {{ $store->address }}<br>
{{ __('checkout.phone') }}: {{ $store->phone }}<br>
{{ __('checkout.hours') }}: {{ $store->hours }}
</div>
</label>
</div>
</div>
</div>
<!-- Continue Button -->
<div class="mt-4">
<form action="{{ route('checkout.delivery.process') }}" method="post">
@csrf
<input type="hidden" name="delivery_method" id="deliveryMethodInput" value="shipping">
<input type="hidden" name="address_id" id="addressIdInput" value="">
<button type="submit" class="btn btn-lg btn-primary w-100"
id="continueButton">
<span
id="continueButtonText">{{ __('checkout.continue_to_shipping') }}</span>
<i class="ci-chevron-right fs-lg ms-1 me-n1"></i>
</a>
</button>
</form>
</div>
</div>
</div>
</div>
<div class="d-flex align-items-start">
<div class="d-flex align-items-start" id="shippingAddressStep">
<div class="d-flex align-items-center justify-content-center bg-body-secondary text-body-secondary rounded-circle fs-sm fw-semibold lh-1 flex-shrink-0"
style="width: 2rem; height: 2rem; margin-top: -.125rem">2</div>
<h2 class="h5 text-body-secondary ps-3 ps-md-4 mb-0">Shipping address</h2>
<h2 class="h5 text-body-secondary ps-3 ps-md-4 mb-0">{{ __('checkout.choose_shipping') }}
</h2>
</div>
<div class="d-flex align-items-start">
<div class="d-flex align-items-center justify-content-center bg-body-secondary text-body-secondary rounded-circle fs-sm fw-semibold lh-1 flex-shrink-0"
style="width: 2rem; height: 2rem; margin-top: -.125rem">3</div>
<h2 class="h5 text-body-secondary ps-3 ps-md-4 mb-0">Payment</h2>
<h2 class="h5 text-body-secondary ps-3 ps-md-4 mb-0">{{ __('checkout.payment') }}</h2>
</div>
</div>
</div>
@ -113,74 +193,8 @@
<!-- Order summary (sticky sidebar) -->
<aside class="col-lg-4 offset-xl-1" style="margin-top: -100px">
<div class="position-sticky top-0" style="padding-top: 100px">
<div class="bg-body-tertiary rounded-5 p-4 mb-3">
<div class="p-sm-2 p-lg-0 p-xl-2">
<div class="border-bottom pb-4 mb-4">
<div class="d-flex align-items-center justify-content-between mb-4">
<h5 class="mb-0">Order summary</h5>
<div class="nav">
<a class="nav-link text-decoration-underline p-0"
href="{{ route('second', ['checkout', 'v1-cart']) }}">Edit</a>
</div>
</div>
<a class="d-flex align-items-center gap-2 text-decoration-none" href="#orderPreview"
data-bs-toggle="offcanvas">
<div class="ratio ratio-1x1" style="max-width: 64px">
<img src="/img/shop/electronics/thumbs/08.png" class="d-block p-1"
alt="iPhone">
</div>
<div class="ratio ratio-1x1" style="max-width: 64px">
<img src="/img/shop/electronics/thumbs/09.png" class="d-block p-1"
alt="iPad Pro">
</div>
<div class="ratio ratio-1x1" style="max-width: 64px">
<img src="/img/shop/electronics/thumbs/01.png" class="d-block p-1"
alt="Smart Watch">
</div>
<i class="ci-chevron-right text-body fs-xl p-0 ms-auto"></i>
</a>
</div>
<ul class="list-unstyled fs-sm gap-3 mb-0">
<li class="d-flex justify-content-between">
Subtotal (3 items):
<span class="text-dark-emphasis fw-medium">$2,427.00</span>
</li>
<li class="d-flex justify-content-between">
Saving:
<span class="text-danger fw-medium">-$110.00</span>
</li>
<li class="d-flex justify-content-between">
Tax collected:
<span class="text-dark-emphasis fw-medium">$73.40</span>
</li>
<li class="d-flex justify-content-between">
Shipping:
<span class="text-dark-emphasis fw-medium">Calculated at checkout</span>
</li>
</ul>
<div class="border-top pt-4 mt-4">
<div class="d-flex justify-content-between mb-3">
<span class="fs-sm">Estimated total:</span>
<span class="h5 mb-0">$2,390.40</span>
</div>
</div>
</div>
</div>
<div class="bg-body-tertiary rounded-5 p-4">
<div class="d-flex align-items-center px-sm-2 px-lg-0 px-xl-2">
<svg class="text-warning flex-shrink-0" xmlns="http://www.w3.org/2000/svg" width="16"
height="16" fill="currentColor">
<path
d="M1.333 9.667H7.5V16h-5c-.64 0-1.167-.527-1.167-1.167V9.667zm13.334 0v5.167c0 .64-.527 1.167-1.167 1.167h-5V9.667h6.167zM0 5.833V7.5c0 .64.527 1.167 1.167 1.167h.167H7.5v-1-3H1.167C.527 4.667 0 5.193 0 5.833zm14.833-1.166H8.5v3 1h6.167.167C15.473 8.667 16 8.14 16 7.5V5.833c0-.64-.527-1.167-1.167-1.167z" />
<path
d="M8 5.363a.5.5 0 0 1-.495-.573C7.752 3.123 9.054-.03 12.219-.03c1.807.001 2.447.977 2.447 1.813 0 1.486-2.069 3.58-6.667 3.58zM12.219.971c-2.388 0-3.295 2.27-3.595 3.377 1.884-.088 3.072-.565 3.756-.971.949-.563 1.287-1.193 1.287-1.595 0-.599-.747-.811-1.447-.811z" />
<path
d="M8.001 5.363c-4.598 0-6.667-2.094-6.667-3.58 0-.836.641-1.812 2.448-1.812 3.165 0 4.467 3.153 4.713 4.819a.5.5 0 0 1-.495.573zM3.782.971c-.7 0-1.448.213-1.448.812 0 .851 1.489 2.403 5.042 2.566C7.076 3.241 6.169.971 3.782.971z" />
</svg>
<div class="text-dark-emphasis fs-sm ps-2 ms-1">Congratulations! You have earned <span
class="fw-semibold">239 bonuses</span></div>
</div>
</div>
<x-checkout.order-summary :subtotal="$subtotal" :total="$total" :savings="0" :tax="0"
:showEdit="true" :editUrl="route('second', ['checkout', 'v1-cart'])" />
</div>
</aside>
</div>
@ -188,8 +202,364 @@
</main>
@include('layouts.partials/footer')
@endsection
@section('scripts')
<script>
document.addEventListener('DOMContentLoaded', function() {
// Delivery method selection
const deliveryOption = document.getElementById('deliveryOption');
const pickupOption = document.getElementById('pickupOption');
const deliveryOptions = document.getElementById('deliveryOptions');
const pickupOptions = document.getElementById('pickupOptions');
const shippingAddress = document.getElementById('shippingAddress');
const continueButton = document.getElementById('continueButton');
// Handle delivery method change
deliveryOption.addEventListener('change', function() {
if (this.checked) {
deliveryOptions.style.display = 'block';
pickupOptions.style.display = 'none';
resetPickupSelection();
// Update button text for delivery
document.getElementById('continueButtonText').textContent = '{{ __("checkout.continue_to_shipping") }}';
// Update hidden input values
document.getElementById('deliveryMethodInput').value = 'delivery';
// Update address ID from selected address
const addressSelect = document.getElementById('addressSelect');
if (addressSelect && addressSelect.value) {
document.getElementById('addressIdInput').value = addressSelect.value;
}
// Show shipping address step and shipping row
const shippingAddressStep = document.getElementById('shippingAddressStep');
const shippingRow = document.getElementById('shipping-row');
if (shippingAddressStep) {
console.log("Showing shipping address step");
shippingAddressStep.style.visibility = 'visible';
shippingAddressStep.style.height = 'auto';
shippingAddressStep.style.overflow = 'visible';
shippingAddressStep.style.margin = '';
shippingAddressStep.style.padding = '';
}
if (shippingRow) {
console.log("Showing shipping row");
shippingRow.style.visibility = 'visible';
shippingRow.style.height = 'auto';
shippingRow.style.overflow = 'visible';
shippingRow.style.margin = '';
shippingRow.style.padding = '';
} else {
console.log("Shipping row not found");
}
}
});
pickupOption.addEventListener('change', function() {
if (this.checked) {
deliveryOptions.style.display = 'none';
pickupOptions.style.display = 'block';
resetDeliverySelection();
// Update button text for pickup
document.getElementById('continueButtonText').textContent =
'{{ __('checkout.continue_to_payment') }}';
// Update hidden input values
document.getElementById('deliveryMethodInput').value = 'pickup';
document.getElementById('addressIdInput').value = '';
// Hide shipping address step and shipping row
const shippingAddressStep = document.getElementById('shippingAddressStep');
const shippingRow = document.getElementById('shipping-row');
console.log("Elements found:", shippingAddressStep, shippingRow);
if (shippingAddressStep) {
console.log("Hiding shipping address step");
shippingAddressStep.style.visibility = 'hidden';
shippingAddressStep.style.height = '0';
shippingAddressStep.style.overflow = 'hidden';
shippingAddressStep.style.margin = '0';
shippingAddressStep.style.padding = '0';
}
if (shippingRow) {
console.log("Hiding shipping row");
shippingRow.style.visibility = 'hidden';
shippingRow.style.height = '0';
shippingRow.style.overflow = 'hidden';
shippingRow.style.margin = '0';
shippingRow.style.padding = '0';
} else {
console.log("Shipping row not found");
}
}
});
// Address selection handler
const addressSelect = document.getElementById('addressSelect');
if (addressSelect) {
// Auto-populate form with first selected address on page load
function populateAddressForm() {
const selectedOption = addressSelect.options[addressSelect.selectedIndex];
if (selectedOption && selectedOption.value) {
// Populate all shipping address form fields
document.getElementById('firstName').value = selectedOption.dataset.firstName || '';
document.getElementById('lastName').value = selectedOption.dataset.lastName || '';
document.getElementById('address').value = selectedOption.dataset.address || '';
document.getElementById('city').value = selectedOption.dataset.city || '';
document.getElementById('state').value = selectedOption.dataset.state || '';
document.getElementById('zip').value = selectedOption.dataset.postcode || '';
document.getElementById('phone').value = selectedOption.dataset.phone || '';
document.getElementById('postcode').value = selectedOption.dataset.postcode || '';
// Update order summary with postcode
if (selectedOption.dataset.postcode) {
updateOrderSummaryWithShipping();
}
}
}
// Populate on page load
populateAddressForm();
// Set initial address ID
if (addressSelect.value) {
document.getElementById('addressIdInput').value = addressSelect.value;
}
addressSelect.addEventListener('change', function() {
const selectedOption = this.options[this.selectedIndex];
if (this.value === 'new' || this.value === '') {
// Clear form fields for new address
document.getElementById('firstName').value = '';
document.getElementById('lastName').value = '';
document.getElementById('address').value = '';
document.getElementById('city').value = '';
document.getElementById('state').value = '';
document.getElementById('zip').value = '';
document.getElementById('phone').value = '';
document.getElementById('postcode').value = '';
// Clear address ID input
document.getElementById('addressIdInput').value = '';
} else {
// Populate form fields with selected address
document.getElementById('firstName').value = selectedOption.dataset.firstName || '';
document.getElementById('lastName').value = selectedOption.dataset.lastName || '';
document.getElementById('address').value = selectedOption.dataset.address || '';
document.getElementById('city').value = selectedOption.dataset.city || '';
document.getElementById('state').value = selectedOption.dataset.state || '';
document.getElementById('zip').value = selectedOption.dataset.postcode || '';
document.getElementById('phone').value = selectedOption.dataset.phone || '';
document.getElementById('postcode').value = selectedOption.dataset.postcode || '';
// Update address ID input
document.getElementById('addressIdInput').value = this.value;
// Update order summary with postcode
if (selectedOption.dataset.postcode) {
updateOrderSummaryWithShipping();
}
}
});
}
// Auto-update order summary when postcode changes
document.getElementById('postcode').addEventListener('input', function() {
if (this.value) {
updateOrderSummaryWithShipping();
}
});
// Store selection for pickup
const storeRadios = document.querySelectorAll('input[name="store"]');
storeRadios.forEach(radio => {
radio.addEventListener('change', function() {
if (this.checked) {
updateOrderSummaryForPickup();
}
});
});
// Form validation before continue
continueButton.addEventListener('click', function(e) {
const selectedMethod = document.querySelector('input[name="deliveryMethod"]:checked').value;
if (selectedMethod === 'delivery') {
if (!validateDeliveryForm()) {
e.preventDefault();
return;
}
} else if (selectedMethod === 'pickup') {
if (!validatePickupForm()) {
e.preventDefault();
return;
}
}
});
function resetDeliverySelection() {
resetShippingCalculation();
}
function resetPickupSelection() {
document.querySelectorAll('input[name="store"]').forEach(radio => {
radio.checked = false;
});
resetShippingCalculation();
}
function resetShippingCalculation() {
// Reset order summary to original state
const shippingElement = document.querySelector('[data-shipping-cost]');
if (shippingElement) {
shippingElement.style.display = 'none';
}
const totalElement = document.getElementById('cart-estimated-total');
if (totalElement && window.originalTotal) {
totalElement.textContent = `Rp ${window.originalTotal}`;
}
}
function updateOrderSummaryWithShipping() {
// Simulate shipping cost calculation based on postcode
const shippingCost = calculateShippingCost(document.getElementById('postcode').value);
// Update order summary
const subtotalElement = document.getElementById('cart-subtotal');
const currentSubtotal = parseFloat(subtotalElement.textContent.replace(/[^\d]/g, ''));
const newTotal = currentSubtotal + shippingCost;
// Store original total
if (!window.originalTotal) {
window.originalTotal = subtotalElement.textContent;
}
// Update total
const totalElement = document.getElementById('cart-estimated-total');
totalElement.textContent = `Rp ${number_format(newTotal, 0, ',', '.')}`;
// Show shipping cost in summary
showShippingCost(shippingCost);
}
function updateOrderSummaryForPickup() {
// Pickup is usually free
const subtotalElement = document.getElementById('cart-subtotal');
const currentSubtotal = parseFloat(subtotalElement.textContent.replace(/[^\d]/g, ''));
// Store original total
if (!window.originalTotal) {
window.originalTotal = subtotalElement.textContent;
}
// Update total (same as subtotal for pickup)
const totalElement = document.getElementById('cart-estimated-total');
totalElement.textContent = subtotalElement.textContent;
// Show free shipping
showShippingCost(0, true);
}
function calculateShippingCost(postcode) {
// Simple shipping cost calculation based on postcode
// In real implementation, this would call an API
const jakartaPostcodes = ['10000', '10110', '10220', '10310', '10410'];
const bandungPostcodes = ['40111', '40112', '40113', '40114', '40115'];
if (jakartaPostcodes.includes(postcode)) {
return 15000; // Jakarta: Rp 15,000
} else if (bandungPostcodes.includes(postcode)) {
return 25000; // Bandung: Rp 25,000
} else {
return 35000; // Other areas: Rp 35,000
}
}
function showShippingCost(cost, isFree = false) {
// Find or create shipping cost element in order summary
let shippingElement = document.querySelector('[data-shipping-cost]');
if (!shippingElement) {
const orderSummary = document.querySelector('.list-unstyled');
const shippingLi = document.createElement('li');
shippingLi.className = 'd-flex justify-content-between';
shippingLi.setAttribute('data-shipping-cost', '');
shippingLi.innerHTML = `
<span>{{ __('checkout.shipping') }}:</span>
<span class="text-dark-emphasis fw-medium" id="shipping-cost">Rp 0</span>
`;
orderSummary.appendChild(shippingLi);
shippingElement = shippingLi;
}
const costElement = document.getElementById('shipping-cost');
if (isFree) {
costElement.textContent = '{{ __('checkout.free') }}';
costElement.className = 'text-success fw-medium';
} else {
costElement.textContent = `Rp ${number_format(cost, 0, ',', '.')}`;
costElement.className = 'text-dark-emphasis fw-medium';
}
shippingElement.style.display = 'flex';
}
function validateDeliveryForm() {
const postcode = document.getElementById('postcode').value;
const firstName = document.getElementById('firstName').value;
const address = document.getElementById('address').value;
const city = document.getElementById('city').value;
const phone = document.getElementById('phone').value;
if (!postcode) {
alert('{{ __('checkout.enter_postcode') }}');
return false;
}
if (!firstName || !address || !city || !phone) {
alert('{{ __('checkout.fill_required_fields') }}');
return false;
}
return true;
}
function validatePickupForm() {
const selectedStore = document.querySelector('input[name="store"]:checked');
if (!selectedStore) {
alert('{{ __('checkout.select_store') }}');
return false;
}
return true;
}
// Number formatting helper
function number_format(number, decimals, dec_point, thousands_sep) {
number = (number + '').replace(/[^0-9+\-Ee.]/g, '');
var n = !isFinite(+number) ? 0 : +number;
var prec = !isFinite(+decimals) ? 0 : Math.abs(decimals);
var sep = (typeof thousands_sep === 'undefined') ? ',' : thousands_sep;
var dec = (typeof dec_point === 'undefined') ? '.' : dec_point;
var s = '';
var toFixedFix = function(n, prec) {
var k = Math.pow(10, prec);
return '' + Math.round(n * k) / k;
};
s = (prec ? toFixedFix(n, prec) : '' + Math.round(n)).split('.');
if (thousands_sep) {
var re = /(-?\d+)(\d{3})/;
while (re.test(s[0])) {
s[0] = s[0].replace(re, '$1' + sep + '$2');
}
}
if ((s[1] || '').length < prec) {
s[1] = s[1] || '';
s[1] += new Array(prec - s[1].length + 1).join('0');
}
return s.join(dec);
}
});
</script>
@endsection

View File

@ -0,0 +1,67 @@
@props([
'subtotal' => 0,
'total' => 0,
'savings' => 0,
'tax' => 0,
'showEdit' => false,
'editUrl' => null,
'showItems' => false,
'items' => [],
])
<div class="bg-body-tertiary rounded-5 p-4 mb-3">
<div class="p-sm-2 p-lg-0 p-xl-2">
<div class="border-bottom pb-4 mb-4">
<div class="d-flex align-items-center justify-content-between mb-4">
<h5 class="mb-0">{{ __('cart_summary.title') }}</h5>
@if ($showEdit && $editUrl)
<div class="nav">
<a class="nav-link text-decoration-underline p-0"
href="{{ $editUrl }}">{{ __('cart_summary.edit') }}</a>
</div>
@endif
</div>
@if ($showItems && count($items) > 0)
<a class="d-flex align-items-center gap-2 text-decoration-none" href="#orderPreview"
data-bs-toggle="offcanvas">
@foreach ($items->take(3) as $item)
<div class="ratio ratio-1x1" style="max-width: 64px">
<img src="{{ $item->image_url ?? '/img/shop/electronics/thumbs/08.png' }}"
class="d-block p-1" alt="{{ $item->name ?? 'Product' }}">
</div>
@endforeach
<i class="ci-chevron-right text-body fs-xl p-0 ms-auto"></i>
</a>
@endif
</div>
<ul class="list-unstyled fs-sm gap-3 mb-0">
<li class="d-flex justify-content-between">
<div>{{ __('cart_summary.subtotal') }} (
{{ __('cart_summary.items_count', ['count' => auth()->check() ? \App\Repositories\Member\Cart\MemberCartRepository::getCount() : 0]) }}):
</div>
<span class="text-dark-emphasis fw-medium" id="cart-subtotal">Rp
{{ number_format($subtotal, 0, ',', '.') }}</span>
</li>
<li class="d-flex justify-content-between">
{{ __('cart_summary.savings') }}:
<span class="text-danger fw-medium">Rp {{ number_format($savings, 0, ',', '.') }}</span>
</li>
<li class="d-flex justify-content-between" id="tax-row"
@if ($tax <= 0) style="display: none;" @endif>
{{ __('cart_summary.tax_collected') }}:
<span class="text-dark-emphasis fw-medium">Rp {{ number_format($tax, 0, ',', '.') }}</span>
</li>
<li class="d-flex justify-content-between" id="shipping-row">
{{ __('cart_summary.shipping') }}:
<span class="text-dark-emphasis fw-medium">{{ __('cart_summary.calculated_at_checkout') }}</span>
</li>
</ul>
<div class="border-top pt-4 mt-4">
<div class="d-flex justify-content-between mb-3">
<span class="fs-sm">{{ __('cart_summary.estimated_total') }}:</span>
<span class="h5 mb-0" id="cart-estimated-total">Rp {{ number_format($total, 0, ',', '.') }}</span>
</div>
</div>
</div>
</div>

View File

@ -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');
@ -95,3 +96,15 @@ Route::middleware(['auth'])->prefix('/cart')->group(function () {
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');
});