¨4.0.1¨

This commit is contained in:
¨NW¨
2023-12-03 14:07:47 +00:00
parent c08b36d1b6
commit f35052522d
1112 changed files with 43019 additions and 24987 deletions

View File

@@ -3,13 +3,16 @@
namespace Modules\Order\Admin;
use Modules\Admin\Ui\AdminTable;
use Illuminate\Http\JsonResponse;
use Yajra\DataTables\Exceptions\Exception;
class OrderTable extends AdminTable
{
/**
* Make table response for the resource.
*
* @return \Illuminate\Http\JsonResponse
* @return JsonResponse
* @throws Exception
*/
public function make()
{

View File

@@ -52,6 +52,7 @@ class CreateOrdersTable extends Migration
});
}
/**
* Reverse the migrations.
*

View File

@@ -26,6 +26,7 @@ class CreateOrderProductsTable extends Migration
});
}
/**
* Reverse the migrations.
*

View File

@@ -24,6 +24,7 @@ class CreateOrderProductOptionsTable extends Migration
});
}
/**
* Reverse the migrations.
*

View File

@@ -24,6 +24,7 @@ class CreateOrderProductOptionValuesTable extends Migration
});
}
/**
* Reverse the migrations.
*

View File

@@ -24,6 +24,7 @@ class CreateOrderTaxesTable extends Migration
});
}
/**
* Reverse the migrations.
*

View File

@@ -18,6 +18,7 @@ class AddValueColumnToOrderProductOptionsTable extends Migration
});
}
/**
* Reverse the migrations.
*

View File

@@ -18,6 +18,7 @@ class AddNoteColumnToOrdersTable extends Migration
});
}
/**
* Reverse the migrations.
*

View File

@@ -15,6 +15,7 @@ class ChangeShippingMethodColumnInOrdersTable extends Migration
DB::statement('ALTER TABLE orders MODIFY shipping_method VARCHAR(191)');
}
/**
* Reverse the migrations.
*

View File

@@ -23,6 +23,7 @@ class CreateOrderDownloadsTable extends Migration
});
}
/**
* Reverse the migrations.
*

View File

@@ -0,0 +1,39 @@
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class CreateOrderProductVariationsTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('order_product_variations', function (Blueprint $table) {
$table->increments('id');
$table->integer('order_product_id')->unsigned();
$table->integer('variation_id')->unsigned();
$table->string('type');
$table->string('value');
$table->unique(['order_product_id', 'variation_id']);
$table->foreign('order_product_id')->references('id')->on('order_products')->onDelete('cascade');
$table->foreign('variation_id')->references('id')->on('variations')->onDelete('cascade');
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('order_product_variations');
}
}

View File

@@ -0,0 +1,37 @@
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class CreateOrderProductVariationValuesTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('order_product_variation_values', function (Blueprint $table) {
$table->integer('order_product_variation_id')->unsigned();
$table->integer('variation_value_id')->unsigned();
$table->primary(['order_product_variation_id', 'variation_value_id'], 'order_product_variation_id_variation_value_id_primary');
$table->foreign('order_product_variation_id', 'order_product_variation_values_order_product_variation_id')->references('id')->on('order_product_variations')->onDelete('cascade');
$table->foreign('variation_value_id')->references('id')->on('variation_values')->onDelete('cascade');
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('order_product_variation_values');
}
}

View File

@@ -0,0 +1,33 @@
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class AddProductVariantIdColumnToOrderProductsTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('order_products', function (Blueprint $table) {
$table->foreignId('product_variant_id')->nullable()->after('product_id');
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('order_products', function (Blueprint $table) {
$table->dropColumn('product_variant_id');
});
}
}

View File

@@ -9,6 +9,7 @@ use Modules\Support\State;
use Modules\Support\Country;
use Modules\Media\Entities\File;
use Modules\Tax\Entities\TaxRate;
use Illuminate\Http\JsonResponse;
use Illuminate\Support\Collection;
use Modules\Order\OrderCollection;
use Modules\Coupon\Entities\Coupon;
@@ -46,26 +47,31 @@ class Order extends Model
*/
protected $dates = ['start_date', 'end_date', 'deleted_at'];
public static function totalSales()
{
return Money::inDefaultCurrency(self::withoutCanceledOrders()->sum('total'));
}
public function status()
{
return trans("order::statuses.{$this->status}");
}
public function hasShippingMethod()
{
return !is_null($this->shipping_method);
}
public function hasCoupon()
{
return !is_null($this->coupon);
}
public function totalTax()
{
$total = 0;
@@ -81,11 +87,23 @@ class Order extends Model
return Money::inDefaultCurrency($total);
}
public function hasTax()
{
return $this->taxes->isNotEmpty();
}
public function taxes()
{
return $this->belongsToMany(TaxRate::class, 'order_taxes')
->using(OrderTax::class)
->as('order_tax')
->withPivot('amount')
->withTrashed();
}
public function salesAnalytics()
{
return $this->normalizeOrders($this->ordersByWeekDay())->mapWithKeys(function ($orders, $weekDay) {
@@ -93,6 +111,198 @@ class Order extends Model
});
}
public function coupon()
{
return $this->belongsTo(Coupon::class)->withTrashed();
}
public function getSubTotalAttribute($subTotal)
{
return Money::inDefaultCurrency($subTotal);
}
public function getShippingCostAttribute($shippingCost)
{
return Money::inDefaultCurrency($shippingCost);
}
public function getDiscountAttribute($discount)
{
return Money::inDefaultCurrency($discount);
}
public function getTaxAttribute($tax)
{
return Money::inDefaultCurrency($tax);
}
public function getTotalAttribute($total)
{
return Money::inDefaultCurrency($total);
}
/**
* Get the order's shipping method.
*
* @param string $shippingMethod
*
* @return string
*/
public function getShippingMethodAttribute($shippingMethod)
{
return ShippingMethod::get($shippingMethod)->label ?? null;
}
/**
* Get the order's payment method.
*
* @param string $paymentMethod
*
* @return string
*/
public function getPaymentMethodAttribute($paymentMethod)
{
return Gateway::get($paymentMethod)->label ?? '';
}
public function getCustomerFullNameAttribute()
{
return "{$this->customer_first_name} {$this->customer_last_name}";
}
public function getBillingFullNameAttribute()
{
return "{$this->billing_first_name} {$this->billing_last_name}";
}
public function getShippingFullNameAttribute()
{
return "{$this->shipping_first_name} {$this->shipping_last_name}";
}
public function getBillingCountryNameAttribute()
{
return Country::name($this->billing_country);
}
public function getShippingCountryNameAttribute()
{
return Country::name($this->shipping_country);
}
public function getBillingStateNameAttribute()
{
return State::name($this->billing_country, $this->billing_state);
}
public function getShippingStateNameAttribute()
{
return State::name($this->shipping_country, $this->shipping_state);
}
public function scopeWithoutCanceledOrders($query)
{
return $query->whereNotIn('status', [self::CANCELED, self::REFUNDED]);
}
public function storeProducts(CartItem $cartItem)
{
$orderProduct = $this->products()->create([
'product_id' => $cartItem->product->id,
'product_variant_id' => $cartItem->variant?->id,
'unit_price' => $cartItem->unitPrice()->amount(),
'qty' => $cartItem->qty,
'line_total' => $cartItem->totalPrice()->amount(),
]);
$orderProduct->storeVariations($cartItem->variations);
$orderProduct->storeOptions($cartItem->options);
}
public function products()
{
return $this->hasMany(OrderProduct::class);
}
public function storeDownloads(CartItem $cartItem)
{
$cartItem->product->downloads->each(function (File $file) {
$this->downloads()->create(['file_id' => $file->id]);
});
}
public function downloads()
{
return $this->hasMany(OrderDownload::class);
}
public function attachTax(CartTax $cartTax)
{
$this->taxes()->attach($cartTax->id(), ['amount' => $cartTax->amount()->amount()]);
}
public function storeTransaction($response)
{
if (!$response instanceof HasTransactionReference) {
return;
}
$this->transaction()->create([
'transaction_id' => $response->getTransactionReference(),
'payment_method' => $this->attributes['payment_method'],
]);
}
public function transaction()
{
return $this->hasOne(Transaction::class)->withTrashed();
}
/**
* Get table data for the resource
*
* @return JsonResponse
*/
public function table()
{
$query = $this->newQuery()->select(['id', 'customer_first_name', 'customer_last_name', 'customer_email', 'currency', 'total', 'status', 'created_at']);
return new OrderTable($query);
}
private function normalizeOrders($orders)
{
return Collection::times(7)->map(function ($dayOfWeek) use ($orders) {
return new OrderCollection($orders[$dayOfWeek] ?? []);
});
}
private function ordersByWeekDay()
{
return self::select('total', 'created_at')
@@ -106,12 +316,6 @@ class Order extends Model
});
}
private function normalizeOrders($orders)
{
return Collection::times(7)->map(function ($dayOfWeek) use ($orders) {
return new OrderCollection($orders[$dayOfWeek] ?? []);
});
}
private function dataForChart(OrderCollection $orders)
{
@@ -120,170 +324,4 @@ class Order extends Model
'total_orders' => $orders->count(),
];
}
public function products()
{
return $this->hasMany(OrderProduct::class);
}
public function downloads()
{
return $this->hasMany(OrderDownload::class);
}
public function coupon()
{
return $this->belongsTo(Coupon::class)->withTrashed();
}
public function taxes()
{
return $this->belongsToMany(TaxRate::class, 'order_taxes')
->using(OrderTax::class)
->as('order_tax')
->withPivot('amount')
->withTrashed();
}
public function transaction()
{
return $this->hasOne(Transaction::class)->withTrashed();
}
public function getSubTotalAttribute($subTotal)
{
return Money::inDefaultCurrency($subTotal);
}
public function getShippingCostAttribute($shippingCost)
{
return Money::inDefaultCurrency($shippingCost);
}
public function getDiscountAttribute($discount)
{
return Money::inDefaultCurrency($discount);
}
public function getTaxAttribute($tax)
{
return Money::inDefaultCurrency($tax);
}
public function getTotalAttribute($total)
{
return Money::inDefaultCurrency($total);
}
/**
* Get the order's shipping method.
*
* @param string $shippingMethod
*
* @return string
*/
public function getShippingMethodAttribute($shippingMethod)
{
return ShippingMethod::get($shippingMethod)->label ?? null;
}
/**
* Get the order's payment method.
*
* @param string $paymentMethod
*
* @return string
*/
public function getPaymentMethodAttribute($paymentMethod)
{
return Gateway::get($paymentMethod)->label ?? '';
}
public function getCustomerFullNameAttribute()
{
return "{$this->customer_first_name} {$this->customer_last_name}";
}
public function getBillingFullNameAttribute()
{
return "{$this->billing_first_name} {$this->billing_last_name}";
}
public function getShippingFullNameAttribute()
{
return "{$this->shipping_first_name} {$this->shipping_last_name}";
}
public function getBillingCountryNameAttribute()
{
return Country::name($this->billing_country);
}
public function getShippingCountryNameAttribute()
{
return Country::name($this->shipping_country);
}
public function getBillingStateNameAttribute()
{
return State::name($this->billing_country, $this->billing_state);
}
public function getShippingStateNameAttribute()
{
return State::name($this->shipping_country, $this->shipping_state);
}
public function scopeWithoutCanceledOrders($query)
{
return $query->whereNotIn('status', [self::CANCELED, self::REFUNDED]);
}
public function storeProducts(CartItem $cartItem)
{
$orderProduct = $this->products()->create([
'product_id' => $cartItem->product->id,
'unit_price' => $cartItem->unitPrice()->amount(),
'qty' => $cartItem->qty,
'line_total' => $cartItem->total()->amount(),
]);
$orderProduct->storeOptions($cartItem->options);
}
public function storeDownloads(CartItem $cartItem)
{
$cartItem->product->downloads->each(function (File $file) {
$this->downloads()->create(['file_id' => $file->id]);
});
}
public function attachTax(CartTax $cartTax)
{
$this->taxes()->attach($cartTax->id(), ['amount' => $cartTax->amount()->amount()]);
}
public function storeTransaction($response)
{
if (!$response instanceof HasTransactionReference) {
return;
}
$this->transaction()->create([
'transaction_id' => $response->getTransactionReference(),
'payment_method' => $this->attributes['payment_method'],
]);
}
/**
* Get table data for the resource
*
* @return \Illuminate\Http\JsonResponse
*/
public function table()
{
$query = $this->newQuery()->select(['id', 'customer_first_name', 'customer_last_name', 'customer_email', 'currency', 'total', 'status', 'created_at']);
return new OrderTable($query);
}
}

View File

@@ -28,16 +28,19 @@ class OrderDownload extends Model
*/
protected $fillable = ['file_id'];
public function getRealPathAttribute()
{
return $this->file->realPath();
}
public function getFilenameAttribute()
{
return $this->file->file_name;
}
public function file()
{
return $this->belongsTo(File::class);

View File

@@ -5,6 +5,8 @@ namespace Modules\Order\Entities;
use Modules\Support\Money;
use Modules\Support\Eloquent\Model;
use Modules\Product\Entities\Product;
use Illuminate\Database\Eloquent\Collection;
class OrderProduct extends Model
{
@@ -20,7 +22,7 @@ class OrderProduct extends Model
*
* @var array
*/
protected $with = ['product', 'options'];
protected $with = ['product', 'options', 'variations'];
/**
* The attributes that aren't mass assignable.
@@ -29,16 +31,25 @@ class OrderProduct extends Model
*/
protected $guarded = [];
public function url()
{
return route('products.show', ['slug' => $this->product->slug]);
}
public function hasAnyOption()
{
return $this->options->isNotEmpty();
}
public function hasAnyVariation()
{
return $this->variations->isNotEmpty();
}
/**
* Determine if order product has been deleted.
*
@@ -49,10 +60,40 @@ class OrderProduct extends Model
return $this->product->trashed();
}
/**
* Store order product's variations.
*
* @param Collection $variations
*
* @return void
*/
public function storeVariations($variations)
{
$variations->each(function ($variation) {
$orderProductVariation = $this->variations()->create([
'order_product_id' => $this->id,
'variation_id' => $variation->id,
'type' => $variation->type,
'value' => $variation->values->first()->label,
]);
$orderProductVariation->storeValues($variation->values);
});
}
public function variations()
{
return $this->hasMany(OrderProductVariation::class);
}
/**
* Store order product's options.
*
* @param \Illuminate\Database\Eloquent\Collection $options
* @param Collection $options
*
* @return void
*/
public function storeOptions($options)
@@ -68,6 +109,13 @@ class OrderProduct extends Model
});
}
public function options()
{
return $this->hasMany(OrderProductOption::class);
}
public function product()
{
return $this->belongsTo(Product::class)
@@ -75,10 +123,6 @@ class OrderProduct extends Model
->withTrashed();
}
public function options()
{
return $this->hasMany(OrderProductOption::class);
}
/**
* Get the order product's name.
@@ -90,6 +134,7 @@ class OrderProduct extends Model
return $this->product->name;
}
/**
* Get the order product's slug.
*
@@ -100,13 +145,26 @@ class OrderProduct extends Model
return $this->product->slug;
}
public function getUnitPriceAttribute($unitPrice)
{
return Money::inDefaultCurrency($unitPrice);
}
public function getLineTotalAttribute($total)
{
return Money::inDefaultCurrency($total);
}
/**
* Get the order product's SKU.
*
* @return string
*/
public function getSkuAttribute()
{
return $this->product_variant ? $this->product_variant->sku : $this->product->sku;
}
}

View File

@@ -29,28 +29,25 @@ class OrderProductOption extends Model
*/
protected $guarded = [];
public function option()
{
return $this->belongsTo(Option::class)->withTrashed();
}
public function values()
{
return $this->belongsToMany(OptionValue::class, 'order_product_option_values')
->using(OrderProductOptionValue::class)
->withPivot('price');
}
public function getNameAttribute()
{
return $this->option->name;
}
public function isFieldType()
{
return $this->option->isFieldType();
}
public function storeValues($product, $values)
{
$values = $values->mapWithKeys(function (OptionValue $value) use ($product) {
@@ -61,4 +58,12 @@ class OrderProductOption extends Model
$this->values()->attach($values->all());
}
public function values()
{
return $this->belongsToMany(OptionValue::class, 'order_product_option_values')
->using(OrderProductOptionValue::class)
->withPivot('price');
}
}

View File

@@ -0,0 +1,56 @@
<?php
namespace Modules\Order\Entities;
use Illuminate\Database\Eloquent\Model;
use Modules\Variation\Entities\Variation;
use Modules\Variation\Entities\VariationValue;
class OrderProductVariation extends Model
{
/**
* Indicates if the model should be timestamped.
*
* @var bool
*/
public $timestamps = false;
/**
* The relations to eager load on every query.
*
* @var array
*/
protected $with = ['variation', 'values'];
/**
* The attributes that aren't mass assignable.
*
* @var array
*/
protected $guarded = [];
public function variation()
{
return $this->belongsTo(Variation::class)->withTrashed();
}
public function getNameAttribute()
{
return $this->variation->name;
}
public function storeValues($values)
{
$this->values()->attach($values->pluck('id'));
}
public function values()
{
return $this->belongsToMany(VariationValue::class, 'order_product_variation_values')
->using(OrderProductVariationValue::class);
}
}

View File

@@ -0,0 +1,10 @@
<?php
namespace Modules\Order\Entities;
use Illuminate\Database\Eloquent\Relations\Pivot;
class OrderProductVariationValue extends Pivot
{
}

View File

@@ -12,14 +12,16 @@ class OrderStatusChanged
/**
* The instance of order.
*
* @var \Modules\Order\Entities\Order
* @var Order
*/
public $order;
/**
* Create a new event instance.
*
* @param \Modules\Order\Entities\Order $order
* @param Order $order
*
* @return void
*/
public function __construct(Order $order)

View File

@@ -2,6 +2,7 @@
namespace Modules\Order\Http\Controllers\Admin;
use Illuminate\Http\Response;
use Modules\Order\Entities\Order;
use Modules\Checkout\Mail\Invoice;
use Illuminate\Support\Facades\Mail;
@@ -11,8 +12,9 @@ class OrderEmailController
/**
* Store a newly created resource in storage.
*
* @param \Modules\Order\Entities\Order $order
* @return \Illuminate\Http\Response
* @param Order $order
*
* @return Response
*/
public function store(Order $order)
{

View File

@@ -2,6 +2,7 @@
namespace Modules\Order\Http\Controllers\Admin;
use Illuminate\Http\Response;
use Modules\Order\Entities\Order;
class OrderPrintController
@@ -9,8 +10,9 @@ class OrderPrintController
/**
* Show the specified resource.
*
* @param \Modules\Order\Entities\Order $order
* @return \Illuminate\Http\Response
* @param Order $order
*
* @return Response
*/
public function show(Order $order)
{

View File

@@ -2,6 +2,7 @@
namespace Modules\Order\Http\Controllers\Admin;
use Illuminate\Http\Response;
use Modules\Order\Entities\Order;
use Modules\Order\Entities\OrderProduct;
use Modules\Order\Events\OrderStatusChanged;
@@ -11,8 +12,9 @@ class OrderStatusController
/**
* Update the specified resource in storage.
*
* @param \Modules\Order\Entities\Order $request
* @return \Illuminate\Http\Response
* @param Order $request
*
* @return Response
*/
public function update(Order $order)
{
@@ -27,6 +29,7 @@ class OrderStatusController
return $message;
}
private function adjustStock(Order $order)
{
if ($this->canceledOrRefunded(request('status'))) {
@@ -38,36 +41,54 @@ class OrderStatusController
}
}
private function canceledOrRefunded($status)
{
return in_array($status, [Order::CANCELED, Order::REFUNDED]);
}
private function restoreStock(Order $order)
{
$order->products->each(function (OrderProduct $orderProduct) {
if ($orderProduct->product->manage_stock) {
$orderProduct->product->increment('qty', $orderProduct->qty);
}
if ($orderProduct->product_variant) {
if ($orderProduct->product_variant->manage_stock) {
$orderProduct->product_variant->increment('qty', $orderProduct->qty);
}
if ($orderProduct->product->qty > 0) {
$orderProduct->product->markAsInStock();
if ($orderProduct->product_variant->qty === 1) {
$orderProduct->product_variant->markAsInStock();
}
} else {
if ($orderProduct->product->manage_stock) {
$orderProduct->product->increment('qty', $orderProduct->qty);
}
if ($orderProduct->product->qty > 0) {
$orderProduct->product->markAsInStock();
}
}
});
}
private function reduceStock(Order $order)
{
$order->products->each(function (OrderProduct $orderProduct) {
if (
$orderProduct->product->manage_stock
&& $orderProduct->product->qty !== 0
) {
$orderProduct->product->decrement('qty', $orderProduct->qty);
}
if ($orderProduct->product->qty === 0) {
$orderProduct->product->markAsOutOfStock();
if ($orderProduct->product_variant) {
if (
$orderProduct->product_variant->manage_stock
&& $orderProduct->product_variant->qty !== 0
) {
$orderProduct->product_variant->decrement('qty', $orderProduct->qty);
}
} else {
if (
$orderProduct->product->manage_stock
&& $orderProduct->product->qty !== 0
) {
$orderProduct->product->decrement('qty', $orderProduct->qty);
}
}
});
}

View File

@@ -2,22 +2,25 @@
namespace Modules\Order\Http\Controllers;
use Illuminate\Http\Response;
class OrderController
{
/**
* Display a listing of the resource.
*
* @return \Illuminate\Http\Response
* @return Response
*/
public function index()
{
return view('order::index');
}
/**
* Show the specified resource.
*
* @return \Illuminate\Http\Response
* @return Response
*/
public function show()
{

View File

@@ -2,10 +2,13 @@
namespace Modules\Order\Http\Requests;
use Exception;
use Modules\Support\Country;
use Modules\Cart\Facades\Cart;
use Illuminate\Validation\Rule;
use Modules\Payment\Facades\Gateway;
use Modules\Core\Http\Requests\Request;
use Modules\Checkout\Http\Exceptions\CheckoutException;
class StoreOrderRequest extends Request
{
@@ -16,16 +19,54 @@ class StoreOrderRequest extends Request
*/
protected $availableAttributes = 'checkout::attributes';
/**
* Validate the class instance.
*
* @return void
* @throws Exception
*/
public function prepareForValidation()
{
if (!Cart::allItemsAreVirtual() && !$this->input('shipping_method')) {
throw new CheckoutException(trans('checkout::messages.no_shipping_method'));
}
}
/**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function rules()
{
return array_merge(
[
'customer_email' => ['required', 'email', $this->emailUniqueRule()],
'customer_phone' => ['required'],
'create_an_account' => 'boolean',
'password' => 'required_if:create_an_account,1',
'ship_to_a_different_address' => 'boolean',
'payment_method' => ['required', Rule::in(Gateway::names())],
'terms_and_conditions' => 'accepted',
'shipping_method' => Cart::allItemsAreVirtual() ? 'nullable' : 'required',
],
$this->billingAddressRules(),
$this->shippingAddressRules()
);
}
private function emailUniqueRule()
{
return $this->create_an_account ? Rule::unique('users', 'email') : null;
}
private function billingAddressRules()
{
return [
'customer_email' => ['required', 'email', $this->emailUniqueRule()],
'customer_phone' => ['required'],
'billing.first_name' => 'required',
'billing.last_name' => 'required',
'billing.address_1' => 'required',
@@ -33,9 +74,13 @@ class StoreOrderRequest extends Request
'billing.zip' => 'required',
'billing.country' => ['required', Rule::in(Country::supportedCodes())],
'billing.state' => 'required',
'create_an_account' => 'boolean',
'password' => 'required_if:create_an_account,1',
'ship_to_a_different_address' => 'boolean',
];
}
private function shippingAddressRules()
{
return [
'shipping.first_name' => 'required_if:ship_to_a_different_address,1',
'shipping.last_name' => 'required_if:ship_to_a_different_address,1',
'shipping.address_1' => 'required_if:ship_to_a_different_address,1',
@@ -43,13 +88,6 @@ class StoreOrderRequest extends Request
'shipping.zip' => 'required_if:ship_to_a_different_address,1',
'shipping.country' => ['required_if:ship_to_a_different_address,1', Rule::in(Country::supportedCodes())],
'shipping.state' => 'required_if:ship_to_a_different_address,1',
'payment_method' => ['required', Rule::in(Gateway::names())],
'terms_and_conditions' => 'accepted',
];
}
private function emailUniqueRule()
{
return $this->create_an_account ? Rule::unique('users', 'email') : null;
}
}

View File

@@ -11,12 +11,13 @@ class SendOrderStatusChangedEmail
/**
* Handle the event.
*
* @param \Modules\Order\Events\OrderStatusChanged $event
* @param OrderStatusChanged $event
*
* @return void
*/
public function handle(OrderStatusChanged $event)
{
if (! in_array($event->order->status, setting('email_order_statuses', []))) {
if (!in_array($event->order->status, setting('email_order_statuses', []))) {
return;
}

View File

@@ -11,12 +11,13 @@ class SendOrderStatusChangedSms
/**
* Handle the event.
*
* @param \Modules\Order\Events\OrderStatusChanged $event
* @param OrderStatusChanged $event
*
* @return void
*/
public function handle(OrderStatusChanged $event)
{
if (! in_array($event->order->status, setting('sms_order_statuses', []))) {
if (!in_array($event->order->status, setting('sms_order_statuses', []))) {
return;
}
@@ -26,6 +27,7 @@ class SendOrderStatusChangedSms
);
}
private function message(Order $order)
{
return trans('sms::messages.order_status_changed', [

View File

@@ -5,6 +5,7 @@ namespace Modules\Order\Mail;
use Illuminate\Bus\Queueable;
use Illuminate\Mail\Mailable;
use Modules\Media\Entities\File;
use Modules\Order\Entities\Order;
use Illuminate\Queue\SerializesModels;
use Illuminate\Contracts\Queue\ShouldQueue;
@@ -15,10 +16,12 @@ class OrderStatusChanged extends Mailable implements ShouldQueue
public $heading;
public $text;
/**
* Create a new message instance.
*
* @param \Modules\Order\Entities\Order $order
* @param Order $order
*
* @return void
*/
public function __construct($order)
@@ -29,11 +32,13 @@ class OrderStatusChanged extends Mailable implements ShouldQueue
$this->text = $this->getText($order);
}
public function getHeading($order)
{
return trans('storefront::mail.hello', ['name' => $order->customer_first_name]);
}
public function getText($order)
{
return trans('order::mail.your_order_status_changed_text', [
@@ -42,6 +47,7 @@ class OrderStatusChanged extends Mailable implements ShouldQueue
]);
}
/**
* Build the message.
*
@@ -55,6 +61,7 @@ class OrderStatusChanged extends Mailable implements ShouldQueue
]);
}
private function getViewName()
{
return 'text' . (is_rtl() ? '_rtl' : '');

View File

@@ -2,6 +2,9 @@
namespace Modules\Order\Providers;
use Modules\Order\Events\OrderStatusChanged;
use Modules\Order\Listeners\SendOrderStatusChangedSms;
use Modules\Order\Listeners\SendOrderStatusChangedEmail;
use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider;
class EventServiceProvider extends ServiceProvider
@@ -12,9 +15,9 @@ class EventServiceProvider extends ServiceProvider
* @var array
*/
protected $listen = [
\Modules\Order\Events\OrderStatusChanged::class => [
\Modules\Order\Listeners\SendOrderStatusChangedEmail::class,
\Modules\Order\Listeners\SendOrderStatusChangedSms::class,
OrderStatusChanged::class => [
SendOrderStatusChangedEmail::class,
SendOrderStatusChangedSms::class,
],
];
}

View File

@@ -2,13 +2,10 @@
namespace Modules\Order\Providers;
use Modules\Support\Traits\AddsAsset;
use Illuminate\Support\ServiceProvider;
class OrderServiceProvider extends ServiceProvider
{
use AddsAsset;
/**
* Bootstrap the application services.
*
@@ -16,6 +13,5 @@ class OrderServiceProvider extends ServiceProvider
*/
public function boot()
{
$this->addAdminAssets('admin.orders.show', ['admin.order.css', 'admin.order.js']);
}
}

View File

@@ -42,7 +42,7 @@
.order-wrapper {
background: #ffffff;
padding: 15px;
padding: 20px;
border-radius: 3px;
.order {
@@ -59,8 +59,7 @@
}
}
h4 {
font-weight: 500;
h5 {
margin-bottom: 10px;
}
@@ -82,28 +81,30 @@
border-bottom: none;
}
> td {
font-size: 16px;
padding-top: 16px;
padding-bottom: 16px;
th {
&:first-child {
padding-left: 0;
}
}
td {
padding-top: 20px;
padding-bottom: 20px;
border-top: 1px solid #f1f1f1 !important;
vertical-align: middle;
&:first-child {
min-width: 250px;
}
&:last-child {
font-weight: 500;
padding-left: 0;
}
}
a {
font-size: 16px;
font-size: 14px;
font-weight: 400;
color: #444444;
letter-spacing: 0.2px;
transition: 200ms ease-in-out;
transition: 150ms ease-in-out;
}
a:hover {
@@ -141,7 +142,7 @@
margin-left: -8px;
tr > td:first-child {
font-family: "Open Sans", sans-serif;
font-family: "Inter", sans-serif;
font-weight: 600;
white-space: nowrap;
}
@@ -170,21 +171,21 @@
tbody > tr {
> td {
font-family: "Roboto", sans-serif !important;
font-family: "Inter", sans-serif !important;
font-weight: 400 !important;
font-size: 17px;
padding: 5px 8px;
font-size: 14px;
padding: 8px 0;
}
&:last-child > td {
font-family: "Roboto", sans-serif;
font-family: "Inter", sans-serif;
font-weight: 500 !important;
border-top: 1px solid #e9e9e9;
}
}
.coupon-code {
font-family: "Open Sans", sans-serif;
font-family: "Inter", sans-serif;
font-weight: 600;
}
}

View File

@@ -5,13 +5,21 @@ html {
}
body {
font-family: 'Open Sans', sans-serif;
font-family: "Inter", sans-serif;
font-size: 15px;
min-width: 350px;
margin: 0;
}
h1, h2, h3, h4, h5, h6, ul, li, p {
h1,
h2,
h3,
h4,
h5,
h6,
ul,
li,
p {
margin: 0;
padding: 0;
color: #444444;
@@ -53,7 +61,7 @@ p {
}
label {
font-weight: 600;
font-weight: 500;
font-size: 16px;
}
@@ -67,7 +75,8 @@ table {
background-color: transparent;
}
td, th {
td,
th {
padding: 0;
}
@@ -76,7 +85,8 @@ td, th {
-moz-box-sizing: border-box;
box-sizing: border-box;
&:before, &:after {
&:before,
&:after {
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;

View File

@@ -1,6 +1,6 @@
<?php
return [
'status_updated' => 'Order status has been updated.',
'invoice_sent' => 'Invoice has been sent to the customer.',
'status_updated' => 'Order status has been updated',
'invoice_sent' => 'Invoice has been sent to the customer',
];

View File

@@ -15,7 +15,6 @@ return [
'order_id' => 'Order ID',
'order_date' => 'Order Date',
'order_status' => 'Order Status',
'shipping_method' => 'Shipping Method',
'payment_method' => 'Payment Method',
'currency' => 'Currency',
'currency_rate' => 'Currency Rate',

View File

@@ -26,9 +26,9 @@
@endsection
@push('scripts')
<script>
<script type="module">
DataTable.setRoutes('#orders-table .table', {
index: '{{ "admin.orders.index" }}',
table: '{{ "admin.orders.table" }}',
show: '{{ "admin.orders.show" }}',
});

View File

@@ -1,10 +1,10 @@
<div class="address-information-wrapper">
<h3 class="section-title">{{ trans('order::orders.address_information') }}</h3>
<h4 class="section-title">{{ trans('order::orders.address_information') }}</h4>
<div class="row">
<div class="col-md-6">
<div class="billing-address">
<h4 class="pull-left">{{ trans('order::orders.billing_address') }}</h4>
<h5 class="pull-left">{{ trans('order::orders.billing_address') }}</h5>
<span>
{{ $order->billing_full_name }}
@@ -26,7 +26,7 @@
<div class="col-md-6">
<div class="shipping-address">
<h4 class="pull-left">{{ trans('order::orders.shipping_address') }}</h4>
<h5 class="pull-left">{{ trans('order::orders.shipping_address') }}</h5>
<span>
{{ $order->shipping_full_name }}

View File

@@ -1,5 +1,5 @@
<div class="items-ordered-wrapper">
<h3 class="section-title">{{ trans('order::orders.items_ordered') }}</h3>
<h4 class="section-title">{{ trans('order::orders.items_ordered') }}</h4>
<div class="row">
<div class="col-md-12">
@@ -25,6 +25,19 @@
<a href="{{ route('admin.products.edit', $product->product->id) }}">{{ $product->name }}</a>
@endif
@if ($product->hasAnyVariation())
<br>
@foreach ($product->variations as $variation)
<span>
{{ $variation->name }}:
<span>
{{ $variation->values()->first()?->label }}{{ $loop->last ? "" : "," }}
</span>
</span>
@endforeach
@endif
@if ($product->hasAnyOption())
<br>
@foreach ($product->options as $option)

View File

@@ -15,12 +15,12 @@
</form>
</div>
<h3 class="section-title">{{ trans('order::orders.order_and_account_information') }}</h3>
<h4 class="section-title">{{ trans('order::orders.order_and_account_information') }}</h4>
<div class="row">
<div class="col-md-6">
<div class="order clearfix">
<h4>{{ trans('order::orders.order_information') }}</h4>
<h5>{{ trans('order::orders.order_information') }}</h5>
<div class="table-responsive">
<table class="table">
@@ -96,7 +96,7 @@
<div class="col-md-6">
<div class="account-information">
<h4>{{ trans('order::orders.account_information') }}</h4>
<h5>{{ trans('order::orders.account_information') }}</h5>
<div class="table-responsive">
<table class="table">

View File

@@ -6,7 +6,7 @@
@endpush
@push('scripts')
<script>
<script type="module">
keypressAction([
{ key: 'b', route: "{{ route('admin.orders.index') }}" }
]);

View File

@@ -1,224 +1,243 @@
<!DOCTYPE html>
<html lang="{{ locale() }}">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{{ trans('order::print.invoice') }}</title>
<title>{{ trans('order::print.invoice') }}</title>
<link href="https://fonts.googleapis.com/css?family=Open+Sans:400,600" rel="stylesheet">
<link href="{{ v(Module::asset('order:admin/css/print.css')) }}" rel="stylesheet">
</head>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&display=swap" rel="stylesheet">
<body class="{{ is_rtl() ? 'rtl' : 'ltr' }}">
<!--[if lt IE 8]>
<p>You are using an <strong>outdated</strong> browser. Please <a href="http://browsehappy.com/">upgrade your browser</a>
to improve your experience.</p>
<![endif]-->
@vite([
'Modules/Order/Resources/assets/admin/sass/print.scss',
])
</head>
<div class="container">
<div class="invoice-wrapper clearfix">
<div class="row">
<div class="invoice-header clearfix">
<div class="col-md-3">
<div class="store-name">
<h1>{{ setting('store_name') }}</h1>
</div>
</div>
<body class="{{ is_rtl() ? 'rtl' : 'ltr' }}">
<!--[if lt IE 8]>
<p>You are using an <strong>outdated</strong> browser. Please <a href="https://browsehappy.com/">upgrade your browser</a>
to improve your experience.</p>
<![endif]-->
<div class="col-md-9 clearfix">
<div class="invoice-header-right pull-right">
<span class="title">{{ trans('order::print.invoice') }}</span>
<div class="invoice-info clearfix">
<div class="invoice-id">
<label for="invoice-id">{{ trans('order::print.invoice_id') }}:</label>
<span>#{{ $order->id }}</span>
<div class="container">
<div class="invoice-wrapper clearfix">
<div class="row">
<div class="invoice-header clearfix">
<div class="col-md-3">
<div class="store-name">
<h1>{{ setting('store_name') }}</h1>
</div>
</div>
<div class="invoice-date">
<label for="invoice-date">{{ trans('order::print.date') }}:</label>
<span>{{ $order->created_at->format('Y / m / d') }}</span>
<div class="col-md-9 clearfix">
<div class="invoice-header-right pull-right">
<span class="title">{{ trans('order::print.invoice') }}</span>
<div class="invoice-info clearfix">
<div class="invoice-id">
<label for="invoice-id">{{ trans('order::print.invoice_id') }}:</label>
<span>#{{ $order->id }}</span>
</div>
<div class="invoice-date">
<label for="invoice-date">{{ trans('order::print.date') }}:</label>
<span>{{ $order->created_at->format('Y / m / d') }}</span>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="invoice-body clearfix">
<div class="invoice-details-wrapper">
<div class="row">
<div class="col-md-6 col-sm-6">
<div class="invoice-details">
<h5>{{ trans('order::print.order_details') }}</h5>
<div class="invoice-body clearfix">
<div class="invoice-details-wrapper">
<div class="row">
<div class="col-md-6 col-sm-6">
<div class="invoice-details">
<h5>{{ trans('order::print.order_details') }}</h5>
<div class="table-responsive">
<table class="table">
<tbody>
<tr>
<td>{{ trans('order::print.email') }}:</td>
<td>{{ $order->customer_email }}</td>
</tr>
<tr>
<td>{{ trans('order::print.phone') }}:</td>
<td>{{ $order->customer_phone }}</td>
</tr>
@if ($order->shipping_method)
<tr>
<td>{{ trans('order::print.shipping_method') }}:</td>
<td>{{ $order->shipping_method }}</td>
</tr>
@endif
<tr>
<td>{{ trans('order::print.payment_method') }}:</td>
<td>{{ $order->payment_method }}
@if($order->payment_method==='Bank Transfer')
</br>
{{setting('bank_transfer_instructions')}}
@endif
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-md-6 col-sm-6">
<div class="invoice-address">
<h5>{{ trans('order::print.shipping_address') }}</h5>
<span>{{ $order->shipping_full_name }}</span>
<span>{{ $order->shipping_address_1 }}</span>
<span>{{ $order->shipping_address_2 }}</span>
<span>{{ $order->shipping_city }}, {{ $order->shipping_state_name }} {{ $order->shipping_zip }}</span>
<span>{{ $order->shipping_country_name }}</span>
</div>
</div>
<div class="col-md-6 col-sm-6">
<div class="invoice-address">
<h5>{{ trans('order::print.billing_address') }}</h5>
<span>{{ $order->billing_full_name }}</span>
<span>{{ $order->billing_address_1 }}</span>
<span>{{ $order->billing_address_2 }}</span>
<span>{{ $order->billing_city }}, {{ $order->billing_state_name }} {{ $order->billing_zip }}</span>
<span>{{ $order->billing_country_name }}</span>
</div>
</div>
</div>
</div>
<div class="row">
<div class="cart-list">
<div class="table-responsive">
<table class="table">
<thead>
<tr>
<th>{{ trans('order::print.product') }}</th>
<th>{{ trans('order::print.unit_price') }}</th>
<th>{{ trans('order::print.quantity') }}</th>
<th>{{ trans('order::print.line_total') }}</th>
</tr>
</thead>
<tbody>
<tr>
<td>{{ trans('order::print.email') }}:</td>
<td>{{ $order->customer_email }}</td>
</tr>
<tr>
<td>{{ trans('order::print.phone') }}:</td>
<td>{{ $order->customer_phone }}</td>
</tr>
@if ($order->shipping_method)
@foreach ($order->products as $product)
<tr>
<td>{{ trans('order::print.shipping_method') }}:</td>
<td>{{ $order->shipping_method }}</td>
</tr>
@endif
<td>
<span>{{ $product->name }}</span>
<tr>
<td>{{ trans('order::print.payment_method') }}:</td>
<td>{{ $order->payment_method }}
@if($order->payment_method==='Bank Transfer')
</br>
{{setting('bank_transfer_instructions')}}
@endif
</td>
</tr>
@if ($product->hasAnyVariation())
<div class="option">
@foreach ($product->variations as $variation)
<span>
{{ $variation->name }}:
<span>
{{ $variation->values()->first()?->label }}{{ $loop->last ? "" : "," }}
</span>
</span>
@endforeach
</div>
@endif
@if ($product->hasAnyOption())
<div class="option">
@foreach ($product->options as $option)
<span>
{{ $option->name }}:
<span>
@if ($option->option->isFieldType())
{{ $option->value }}
@else
{{ $option->values->implode('label', ', ') }}
@endif
</span>
</span>
@endforeach
</div>
@endif
</td>
<td>
<label class="visible-xs">{{ trans('order::print.unit_price') }}:</label>
<span>{{ $product->unit_price->convert($order->currency, $order->currency_rate)->convert($order->currency, $order->currency_rate)->format($order->currency) }}</span>
</td>
<td>
<label class="visible-xs">{{ trans('order::print.quantity') }}:</label>
<span>{{ $product->qty }}</span>
</td>
<td>
<label class="visible-xs">{{ trans('order::print.line_total') }}:</label>
<span>{{ $product->line_total->convert($order->currency, $order->currency_rate)->format($order->currency) }}</span>
</td>
</tr>
@endforeach
</tbody>
</table>
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-md-6 col-sm-6">
<div class="invoice-address">
<h5>{{ trans('order::print.shipping_address') }}</h5>
<span>{{ $order->shipping_full_name }}</span>
<span>{{ $order->shipping_address_1 }}</span>
<span>{{ $order->shipping_address_2 }}</span>
<span>{{ $order->shipping_city }}, {{ $order->shipping_state_name }} {{ $order->shipping_zip }}</span>
<span>{{ $order->shipping_country_name }}</span>
</div>
</div>
<div class="col-md-6 col-sm-6">
<div class="invoice-address">
<h5>{{ trans('order::print.billing_address') }}</h5>
<span>{{ $order->billing_full_name }}</span>
<span>{{ $order->billing_address_1 }}</span>
<span>{{ $order->billing_address_2 }}</span>
<span>{{ $order->billing_city }}, {{ $order->billing_state_name }} {{ $order->billing_zip }}</span>
<span>{{ $order->billing_country_name }}</span>
</div>
</div>
</div>
</div>
<div class="row">
<div class="cart-list">
<div class="table-responsive">
<table class="table">
<thead>
<tr>
<th>{{ trans('order::print.product') }}</th>
<th>{{ trans('order::print.unit_price') }}</th>
<th>{{ trans('order::print.quantity') }}</th>
<th>{{ trans('order::print.line_total') }}</th>
</tr>
</thead>
<tbody>
@foreach ($order->products as $product)
<div class="total pull-right">
<table class="table">
<tbody>
<tr>
<td>
<span>{{ $product->name }}</span>
@if ($product->hasAnyOption())
<div class="option">
@foreach ($product->options as $option)
<span>
{{ $option->name }}:
<span>
@if ($option->option->isFieldType())
{{ $option->value }}
@else
{{ $option->values->implode('label', ', ') }}
@endif
</span>
</span>
@endforeach
</div>
@endif
</td>
<td>
<label class="visible-xs">{{ trans('order::print.unit_price') }}:</label>
<span>{{ $product->unit_price->convert($order->currency, $order->currency_rate)->convert($order->currency, $order->currency_rate)->format($order->currency) }}</span>
</td>
<td>
<label class="visible-xs">{{ trans('order::print.quantity') }}:</label>
<span>{{ $product->qty }}</span>
</td>
<td>
<label class="visible-xs">{{ trans('order::print.line_total') }}:</label>
<span>{{ $product->line_total->convert($order->currency, $order->currency_rate)->format($order->currency) }}</span>
</td>
<td>{{ trans('order::print.subtotal') }}</td>
<td>{{ $order->sub_total->convert($order->currency, $order->currency_rate)->format($order->currency) }}</td>
</tr>
@endforeach
</tbody>
</table>
@if ($order->hasShippingMethod())
<tr>
<td>{{ $order->shipping_method }}</td>
<td>{{ $order->shipping_cost->convert($order->currency, $order->currency_rate)->format($order->currency) }}</td>
</tr>
@endif
@if ($order->hasCoupon())
<tr>
<td>
{{ trans('order::orders.coupon') }} <span class="coupon-code">({{ $order->coupon->code }})</span>
</td>
<td>
&#8211;{{ $order->discount->convert($order->currency, $order->currency_rate)->format($order->currency) }}</td>
</tr>
@endif
@foreach ($order->taxes as $tax)
<tr>
<td>{{ $tax->name }}</td>
<td class="text-right">{{ $tax->order_tax->amount->convert($order->currency, $order->currency_rate)->format($order->currency) }}</td>
</tr>
@endforeach
<tr>
<td>{{ trans('order::print.total') }}</td>
<td>{{ $order->total->convert($order->currency, $order->currency_rate)->format($order->currency) }}</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<div class="total pull-right">
<table class="table">
<tbody>
<tr>
<td>{{ trans('order::print.subtotal') }}</td>
<td>{{ $order->sub_total->convert($order->currency, $order->currency_rate)->format($order->currency) }}</td>
</tr>
@if ($order->hasShippingMethod())
<tr>
<td>{{ $order->shipping_method }}</td>
<td>{{ $order->shipping_cost->convert($order->currency, $order->currency_rate)->format($order->currency) }}</td>
</tr>
@endif
@if ($order->hasCoupon())
<tr>
<td>{{ trans('order::orders.coupon') }} (<span
class="coupon-code">{{ $order->coupon->code }}</span>)
</td>
<td>
&#8211;{{ $order->discount->convert($order->currency, $order->currency_rate)->format($order->currency) }}</td>
</tr>
@endif
@foreach ($order->taxes as $tax)
<tr>
<td>{{ $tax->name }}</td>
<td class="text-right">{{ $tax->order_tax->amount->convert($order->currency, $order->currency_rate)->format($order->currency) }}</td>
</tr>
@endforeach
<tr>
<td>{{ trans('order::print.total') }}</td>
<td>{{ $order->total->convert($order->currency, $order->currency_rate)->format($order->currency) }}</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
<script>
window.print();
</script>
</body>
<script type="module">
window.print();
</script>
</body>
</html>

View File

@@ -15,3 +15,10 @@
@include('order::admin.orders.partials.order_totals')
</div>
@endsection
@push('globals')
@vite([
'Modules/Order/Resources/assets/admin/sass/main.scss',
'Modules/Order/Resources/assets/admin/js/main.js',
])
@endpush

View File

@@ -14,6 +14,12 @@ Route::get('orders/{id}', [
'middleware' => 'can:admin.orders.show',
]);
Route::get('orders/index/table', [
'as' => 'admin.orders.table',
'uses' => 'OrderController@table',
'middleware' => 'can:admin.orders.index',
]);
Route::put('orders/{order}/status', [
'as' => 'admin.orders.status.update',
'uses' => 'OrderStatusController@update',