type, $allowedTypes, true)) { return null; } $business_id = (int) $transaction->business_id; $existing = Shipment::where('business_id', $business_id) ->where('transaction_id', $transaction->id) ->first(); if ($existing) { return $this->customsSyncService->linkByTransactionForShipment($existing); } $shipment_type = match ($transaction->type) { 'sell_transfer' => 'stock_transfer', 'purchase' => 'import', 'sell' => 'sell', default => 'manual', }; $status = $transaction->type === 'purchase' ? 'at_customs' : 'scheduled'; $shipment = Shipment::create([ 'business_id' => $business_id, 'ref_no' => $this->tmsUtil->generateRefNo($business_id), 'shipment_type' => $shipment_type, 'transaction_id' => $transaction->id, 'pjt_project_id' => $transaction->pjt_project_id ?? ($extra['pjt_project_id'] ?? null), 'contact_id' => $transaction->contact_id, 'origin_location_id' => $transaction->location_id, 'destination_address' => $transaction->shipping_address, 'status' => $status, 'scheduled_at' => $transaction->delivery_date ?? now(), 'freight_cost' => $transaction->shipping_charges ?? 0, 'cargo_description' => $transaction->shipping_details, 'created_by' => auth()->id(), ...$extra, ]); $this->applyDestinationCoords($shipment); $this->logStatus($shipment, $status, __('tms::lang.auto_created_from_transaction')); $settings = $this->tmsUtil->getTmsSettings($business_id); if (! empty($settings['sync_transaction_shipping'])) { $this->syncToTransaction($shipment); } return $this->customsSyncService->linkByTransactionForShipment($shipment); } public function createFromFieldMission($mission, array $extra = []): ?Shipment { if (! class_exists(\Modules\Maintenance\Models\FieldMission::class)) { return null; } $business_id = (int) $mission->business_id; $existing = $this->missionSyncService->findShipmentForMission($mission); if ($existing) { return $existing; } $destination = $this->missionSyncService->resolveMissionDestination($mission); if (empty($destination)) { throw new \InvalidArgumentException(__('tms::lang.mission_destination_required')); } $shipment = Shipment::create([ 'business_id' => $business_id, 'ref_no' => $this->tmsUtil->generateRefNo($business_id), 'shipment_type' => 'field_service', 'source_type' => 'field_mission', 'source_id' => $mission->id, 'pjt_project_id' => $this->missionSyncService->resolveProjectIdFromMission($mission), 'driver_id' => $this->missionSyncService->resolveDriverId($mission), 'destination_address' => $destination, 'status' => in_array($mission->status, ['dispatched', 'on_site'], true) ? 'in_transit' : 'scheduled', 'scheduled_at' => $mission->planned_departure_at ?? now(), 'cargo_description' => $this->missionSyncService->buildCargoDescription($mission), 'notes' => $mission->notes, 'created_by' => auth()->id(), ...$extra, ]); $this->applyDestinationCoords($shipment); $this->logStatus($shipment, $shipment->status, __('tms::lang.auto_created_from_mission', ['ref' => $mission->mission_number])); return $shipment->fresh(); } public function createFromProject(int $business_id, int $project_id, array $extra = []): Shipment { return Shipment::create([ 'business_id' => $business_id, 'ref_no' => $this->tmsUtil->generateRefNo($business_id), 'shipment_type' => 'project_delivery', 'pjt_project_id' => $project_id, 'status' => 'draft', 'created_by' => auth()->id(), ...$extra, ]); } protected function applyDestinationCoords(Shipment $shipment): void { if ($shipment->destination_latitude && $shipment->destination_longitude) { return; } if (empty($shipment->destination_address)) { return; } $coords = $this->geocodingService->geocode($shipment->destination_address); if ($coords) { $shipment->update([ 'destination_latitude' => $coords['lat'], 'destination_longitude' => $coords['lng'], ]); } } public function updateStatus(Shipment $shipment, string $status, ?string $note = null, ?string $location = null, bool $skipCustomsCheck = false): Shipment { if (! $skipCustomsCheck && $status === 'in_transit' && $this->customsSyncService->customsBlocksTransit($shipment)) { throw new \InvalidArgumentException($this->customsSyncService->transitBlockMessage($shipment)); } $updates = ['status' => $status]; if ($status === 'in_transit' && ! $shipment->pickup_at) { $updates['pickup_at'] = now(); } if ($status === 'delivered') { $updates['delivered_at'] = now(); } $shipment->update($updates); $this->logStatus($shipment, $status, $note, $location); $settings = $this->tmsUtil->getTmsSettings($shipment->business_id); if (! empty($settings['sync_transaction_shipping'])) { $this->syncToTransaction($shipment); } if ($status === 'delivered') { $this->accountingService->postFreightOnDelivery( $shipment->fresh(), (int) (auth()->id() ?? $shipment->created_by ?? 0) ); } $this->missionSyncService->syncMissionFromShipment($shipment->fresh()); return $shipment->fresh(); } public function syncToTransaction(Shipment $shipment): void { if (! $shipment->transaction_id) { return; } $transaction = Transaction::find($shipment->transaction_id); if (! $transaction) { return; } $shipping_status = $this->tmsUtil->mapStatusToTransactionShipping($shipment->status); if (! $shipping_status) { return; } $updates = ['shipping_status' => $shipping_status]; if ($shipment->driver_id && $shipment->driver?->user_id) { $updates['delivery_person'] = $shipment->driver->user_id; } if ($shipment->delivered_at) { $updates['delivery_date'] = $shipment->delivered_at; } if ($shipment->destination_address) { $updates['shipping_address'] = $shipment->destination_address; } if ($shipment->freight_cost) { $updates['shipping_charges'] = $shipment->freight_cost; } $transaction->update($updates); } public function logStatus(Shipment $shipment, string $status, ?string $note = null, ?string $location = null): ShipmentStatusLog { return ShipmentStatusLog::create([ 'shipment_id' => $shipment->id, 'status' => $status, 'note' => $note, 'location' => $location, 'created_by' => auth()->id(), 'logged_at' => now(), ]); } }