feat: Implement Slack, Trello, and Courier (ShipLogic) integration

- Add 14 domain events for order lifecycle (OrderCreated, OrderPacked, ShipmentCreated, ParcelDelivered, etc.)
- Create SlackNotifierService with channels for orders, design, production, shipping, ops-alerts
- Create TrelloService to create cards, move cards between lists, attach files, check items
- Create CourierService to integrate with ShipLogic API for shipment creation and document retrieval
- Create PackingController to explicitly capture packing dimensions and weight
- Create ShippingController with multi-layer guards: packing validation, payment/approval verification, idempotency
- Create TrelloWebhookController to handle incoming Trello webhooks as intent signals
- Create CourierWebhookController to handle Shiplogic status updates
- Create event listeners for Slack notifications and Trello card updates
- Create EventServiceProvider to register all events and listeners
- Add database migration for packing, courier, and Trello data columns
- Create config files for slack, trello, and courier integration
- Update .env with integration secrets placeholders
- Add routes for /orders/{id}/pack, /orders/{id}/ship, /api/webhooks/trello, /api/webhooks/courier

Key architectural decisions:
- Packing is explicit ops action (not automatic from status)
- Shipment creation only after: Trello intent + packing confirmed + payment/approval rules met
- Courier API failures keep order in Ready to Ship state (safe retry)
- Trello and Slack are mirrors of backend state, not decision makers
- All side effects flow through event listeners, maintaining separation of concerns
This commit is contained in:
twotalesanimation
2026-01-02 13:47:31 +02:00
parent 4730f5770c
commit 12aadfd917
35 changed files with 1815 additions and 0 deletions
@@ -0,0 +1,104 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::table('orders', function (Blueprint $table) {
// Packing data
$table->decimal('packing_width', 8, 2)->nullable()->comment('Width in cm');
$table->decimal('packing_length', 8, 2)->nullable()->comment('Length in cm');
$table->decimal('packing_weight', 8, 2)->nullable()->comment('Weight in kg');
$table->timestamp('packing_completed_at')->nullable();
$table->foreignId('packed_by')->nullable()->constrained('users');
// Courier integration
$table->string('courier_waybill_id')->nullable()->unique();
$table->string('courier_tracking_number')->nullable();
$table->string('courier_status')->nullable()->default('pending'); // pending, awaiting_collection, in_transit, delivered, failed
$table->timestamp('delivered_at')->nullable();
$table->text('delivery_failure_reason')->nullable();
// Trello integration
$table->string('trello_card_id')->nullable();
// QR code (for later)
$table->string('qr_token')->nullable()->unique();
$table->timestamp('qr_generated_at')->nullable();
$table->index('courier_waybill_id');
});
Schema::table('custom_orders', function (Blueprint $table) {
// Packing data
$table->decimal('packing_width', 8, 2)->nullable()->comment('Width in cm');
$table->decimal('packing_length', 8, 2)->nullable()->comment('Length in cm');
$table->decimal('packing_weight', 8, 2)->nullable()->comment('Weight in kg');
$table->timestamp('packing_completed_at')->nullable();
$table->foreignId('packed_by')->nullable()->constrained('users');
// Courier integration
$table->string('courier_waybill_id')->nullable()->unique();
$table->string('courier_tracking_number')->nullable();
$table->string('courier_status')->nullable()->default('pending');
$table->timestamp('delivered_at')->nullable();
$table->text('delivery_failure_reason')->nullable();
// Trello integration
$table->string('trello_card_id')->nullable();
// QR code (for later)
$table->string('qr_token')->nullable()->unique();
$table->timestamp('qr_generated_at')->nullable();
$table->index('courier_waybill_id');
});
}
public function down(): void
{
Schema::table('orders', function (Blueprint $table) {
$table->dropForeign(['packed_by']);
$table->dropIndex(['courier_waybill_id']);
$table->dropColumn([
'packing_width',
'packing_length',
'packing_weight',
'packing_completed_at',
'packed_by',
'courier_waybill_id',
'courier_tracking_number',
'courier_status',
'delivered_at',
'delivery_failure_reason',
'trello_card_id',
'qr_token',
'qr_generated_at',
]);
});
Schema::table('custom_orders', function (Blueprint $table) {
$table->dropForeign(['packed_by']);
$table->dropIndex(['courier_waybill_id']);
$table->dropColumn([
'packing_width',
'packing_length',
'packing_weight',
'packing_completed_at',
'packed_by',
'courier_waybill_id',
'courier_tracking_number',
'courier_status',
'delivered_at',
'delivery_failure_reason',
'trello_card_id',
'qr_token',
'qr_generated_at',
]);
});
}
};