Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/admin/resources/lang/en/pages/orders.php
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@
],

'notifications' => [
'shipment_empty' => 'These items are already on another shipment. Refresh the order and try again.',
'archived' => 'The orders has successfully archived !',
'cancelled' => 'The order has successfully cancelled !',
'note_added' => 'Your note has been added to this order.',
Expand Down
1 change: 1 addition & 0 deletions packages/admin/resources/lang/fr/pages/orders.php
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@
],

'notifications' => [
'shipment_empty' => 'Ces articles sont déjà rattachés à une autre expédition. Actualisez la commande et réessayez.',
'archived' => 'La commande a été archivée avec succès !',
'cancelled' => 'La commande a été annulée avec succès !',
'note_added' => 'Votre note a été ajoutée à cette commande.',
Expand Down
45 changes: 21 additions & 24 deletions packages/admin/src/Livewire/SlideOvers/CreateShippingLabel.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,11 @@
use Livewire\Attributes\Computed;
use Livewire\Attributes\Locked;
use Shopper\Contracts\SlideOverForm;
use Shopper\Core\Actions\CreateShipmentAction;
use Shopper\Core\Enum\FulfillmentStatus;
use Shopper\Core\Enum\ShipmentStatus;
use Shopper\Core\Events\Orders\OrderShipmentCreated;
use Shopper\Core\Exceptions\CannotCreateEmptyShipmentException;
use Shopper\Core\Models\Contracts\Order;
use Shopper\Core\Models\OrderItem;
use Shopper\Core\Models\OrderShipping;
use Shopper\Shipping\Services\CarrierRateService;
use Shopper\Traits\HandlesAuthorizationExceptions;
use Shopper\Traits\InteractsWithSlideOverForm;
Expand Down Expand Up @@ -192,27 +191,25 @@ public function save(): void

$data = $this->form->getState();

$itemIds = collect($data['items'])->pluck('item_id')->all();

$shipment = OrderShipping::query()->create([
'order_id' => $this->order->id,
'carrier_id' => $data['carrier_id'],
'status' => ShipmentStatus::Pending,
'tracking_number' => $data['tracking_number'] ?? null,
'tracking_url' => $data['tracking_url'] ?? null,
]);

$shipment->logEvent(ShipmentStatus::Pending, [
'description' => __('shopper::notifications.shipments.label_created'),
]);

$this->order->items()
->whereIn('id', $itemIds)
->update([
'order_shipping_id' => $shipment->id,
]);

event(new OrderShipmentCreated($this->order, $shipment));
try {
(new CreateShipmentAction)->execute(
order: $this->order,
carrierId: (int) $data['carrier_id'],
itemIds: array_map('intval', collect($data['items'])->pluck('item_id')->all()),
trackingNumber: $data['tracking_number'] ?? null,
trackingUrl: $data['tracking_url'] ?? null,
description: __('shopper::notifications.shipments.label_created'),
);
} catch (CannotCreateEmptyShipmentException) {
Notification::make()
->title(__('shopper::pages/orders.notifications.shipment_empty'))
->warning()
->send();

$this->closePanel();

return;
}

Notification::make()
->title(__('shopper::pages/orders.notifications.shipment_created'))
Expand Down
1 change: 1 addition & 0 deletions packages/api/routes/store.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,5 +18,6 @@
});

ShopperApi::webhooks(function (): void {
require __DIR__.'/store/shipping.php';
require __DIR__.'/store/payment.php';
});
8 changes: 8 additions & 0 deletions packages/api/routes/store/shipping.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<?php

declare(strict_types=1);

use Illuminate\Support\Facades\Route;
use Shopper\Api\Http\Controllers\Shipping\WebhookController;

Route::post('/webhooks/shipping/{driver}', WebhookController::class);
43 changes: 43 additions & 0 deletions packages/api/src/Http/Controllers/Shipping/WebhookController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
<?php

declare(strict_types=1);

namespace Shopper\Api\Http\Controllers\Shipping;

use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Shopper\Shipping\Actions\ApplyTrackingInfoAction;
use Shopper\Shipping\Facades\Shipping;
use Throwable;

readonly class WebhookController
{
public function __construct(
private ApplyTrackingInfoAction $action,
) {}

public function __invoke(Request $request, string $driver): JsonResponse
{
if (! in_array($driver, Shipping::availableDrivers(), strict: true) || ! Shipping::isConfigured($driver)) {
abort(404);
}

$shippingDriver = Shipping::driver($driver);

if (! $shippingDriver->supportsWebhooks()) {
abort(404);
}

try {
$info = $shippingDriver->handleWebhook($request);
} catch (Throwable) {
return response()->json(['error' => 'Invalid webhook payload.'], 400);
}

if ($info !== null) {
$this->action->apply($driver, $info);
}

return response()->json(['received' => true]);
}
}
8 changes: 8 additions & 0 deletions packages/core/config/webhooks.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@
use Shopper\Core\Events\Orders\OrderCompleted;
use Shopper\Core\Events\Orders\OrderCreated;
use Shopper\Core\Events\Orders\OrderPaid;
use Shopper\Core\Events\Orders\OrderShipmentCreated;
use Shopper\Core\Events\Orders\OrderShipmentDelivered;
use Shopper\Core\Events\Orders\OrderShipmentDeliveryFailed;
use Shopper\Core\Events\Orders\OrderShipmentReturned;
use Shopper\Core\Events\Orders\OrderShipped;
use Shopper\Core\Events\Products\ProductCreated;
use Shopper\Core\Events\Products\ProductDeleted;
Expand All @@ -32,6 +36,10 @@
OrderCancelled::class => WebhookEventType::OrderCancelled->value,
OrderShipped::class => WebhookEventType::OrderShipped->value,
OrderCompleted::class => WebhookEventType::OrderCompleted->value,
OrderShipmentCreated::class => WebhookEventType::ShipmentCreated->value,
OrderShipmentDelivered::class => WebhookEventType::ShipmentDelivered->value,
OrderShipmentDeliveryFailed::class => WebhookEventType::ShipmentDeliveryFailed->value,
OrderShipmentReturned::class => WebhookEventType::ShipmentReturned->value,
ProductCreated::class => WebhookEventType::ProductCreated->value,
ProductUpdated::class => WebhookEventType::ProductUpdated->value,
ProductDeleted::class => WebhookEventType::ProductDeleted->value,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
<?php

declare(strict_types=1);

use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
use Shopper\Core\Helpers\Migration;

return new class extends Migration
{
public function up(): void
{
Schema::table($this->getTableName('order_shipping_events'), function (Blueprint $table): void {
$table->string('external_id')->nullable()->after('status');
$table->string('source', 16)->default('manual')->after('external_id');
$table->unsignedBigInteger('causer_id')->nullable()->after('source');
$table->unique(['order_shipping_id', 'external_id'], $this->uniqueIndexName());
});

Schema::table($this->getTableName('order_shipping'), static function (Blueprint $table): void {
$table->index('tracking_number');
$table->index('status');
});
}

public function down(): void
{
Schema::table($this->getTableName('order_shipping'), static function (Blueprint $table): void {
$table->dropIndex(['status']);
$table->dropIndex(['tracking_number']);
});

Schema::table($this->getTableName('order_shipping_events'), function (Blueprint $table): void {
$table->dropUnique($this->uniqueIndexName());
$table->dropColumn(['external_id', 'source', 'causer_id']);
});
}

private function uniqueIndexName(): string
{
return $this->getTableName('order_shipping_events').'_external_id_unique';
}
};
35 changes: 35 additions & 0 deletions packages/core/src/Actions/CompleteOrderIfFulfilledAction.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
<?php

declare(strict_types=1);

namespace Shopper\Core\Actions;

use Illuminate\Database\Eloquent\Builder;
use Shopper\Core\Enum\FulfillmentStatus;
use Shopper\Core\Enum\OrderStatus;
use Shopper\Core\Events\Orders\OrderCompleted;
use Shopper\Core\Models\Contracts\Order;

final class CompleteOrderIfFulfilledAction
{
public function execute(Order $order): void
{
if ($order->status !== OrderStatus::Processing || ! $order->isPaid()) {
return;
}

$undelivered = $order->items()
->where(fn (Builder $query) => $query
->whereNull('fulfillment_status')
->orWhere('fulfillment_status', '!=', FulfillmentStatus::Delivered))
->exists();

if ($undelivered) {
return;
}

$order->transitionTo(OrderStatus::Completed);

event(new OrderCompleted($order));
}
}
55 changes: 55 additions & 0 deletions packages/core/src/Actions/CreateShipmentAction.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
<?php

declare(strict_types=1);

namespace Shopper\Core\Actions;

use Illuminate\Support\Facades\DB;
use Shopper\Core\Enum\ShipmentStatus;
use Shopper\Core\Events\Orders\OrderShipmentCreated;
use Shopper\Core\Exceptions\CannotCreateEmptyShipmentException;
use Shopper\Core\Models\Contracts\Order;
use Shopper\Core\Models\OrderShipping;

final class CreateShipmentAction
{
/**
* @param list<int> $itemIds
*/
public function execute(
Order $order,
?int $carrierId,
array $itemIds,
?string $trackingNumber = null,
?string $trackingUrl = null,
?string $description = null,
): OrderShipping {
return DB::transaction(function () use ($order, $carrierId, $itemIds, $trackingNumber, $trackingUrl, $description): OrderShipping {
$shipment = OrderShipping::query()->create([
'order_id' => $order->id,
'carrier_id' => $carrierId,
'status' => ShipmentStatus::Pending,
'tracking_number' => $trackingNumber,
'tracking_url' => $trackingUrl,
]);

$shipment->logEvent(ShipmentStatus::Pending, [
'description' => $description,
'causer_id' => auth()->id(),
]);

$attached = $order->items()
->whereIn('id', $itemIds)
->whereNull('order_shipping_id')
->update(['order_shipping_id' => $shipment->id]);

if ($attached === 0) {
throw CannotCreateEmptyShipmentException::forOrder($order->id);
}

event(new OrderShipmentCreated($order, $shipment));

return $shipment;
});
}
}
3 changes: 0 additions & 3 deletions packages/core/src/Actions/MarkShipmentDeliveredAction.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
namespace Shopper\Core\Actions;

use Shopper\Core\Enum\ShipmentStatus;
use Shopper\Core\Events\Orders\OrderShipmentDelivered;
use Shopper\Core\Models\OrderShipping;

final class MarkShipmentDeliveredAction
Expand All @@ -26,7 +25,5 @@ public function execute(OrderShipping $shipment, array $context = []): void
}

(new RecordShipmentEventAction)->execute($shipment, ShipmentStatus::Delivered, $context);

event(new OrderShipmentDelivered($shipment->order, $shipment));
}
}
Loading