Introduce a comprehensive set of commercial features including a multi-step order approval workflow, customer credit management, and specialized import service invoicing. Key changes: - Implement `pending_approval` and `approved` shipment statuses to allow staff verification before customer payment. - Add a credit system to `User` model with `credit_limit` and `credit_used` to manage customer balances and debts. - Develop a new `importInvoice` PDF generation service following the "Sheet ENG Invoice" specification for import services. - Add Filament resources for managing Audit Logs, Commitment Forms, Customer Credits, and Shipment Checklists. - Implement staff-specific APIs for order approval/rejection and customer financial status monitoring. - Integrate Kavenegar SMS service for mobile verification and notifications. - Add bulk tracking import functionality via CSV/Excel. - Update WordPress bridge assets (CSS/JS) to support the new multi-step order form UI and updated redirection logic. - Update deployment configurations and documentation to reflect new production domains and feature sets.
368 lines
14 KiB
PHP
368 lines
14 KiB
PHP
<?php
|
||
|
||
namespace App\Services;
|
||
|
||
use App\Enums\ShipmentType;
|
||
use App\Models\Shipment;
|
||
use App\Models\ShipmentItem;
|
||
use Dompdf\Dompdf;
|
||
use Dompdf\Options;
|
||
use Illuminate\Support\Collection;
|
||
use Illuminate\Support\Facades\Log;
|
||
use Picqer\Barcode\BarcodeGeneratorPNG;
|
||
|
||
class PdfService
|
||
{
|
||
/**
|
||
* تولید AWB PDF (برای همه نوع محمولهها)
|
||
*/
|
||
public function awb(Shipment $shipment): string
|
||
{
|
||
$shipment->loadMissing(['fromCountry', 'toCountry', 'items']);
|
||
|
||
$data = [
|
||
'shipment' => $shipment,
|
||
'shipper' => $this->formatAddress($shipment, 'sender'),
|
||
'receiver' => $this->formatAddress($shipment, 'receiver'),
|
||
'items' => $shipment->items ?? collect(),
|
||
'invoice_total_usd' => optional($shipment->items)->sum('total_usd') ?? 0,
|
||
'barcode_base64' => $this->generateBarcode($shipment->awb_no),
|
||
'service_label' => $this->getServiceLabel($shipment),
|
||
'type_label' => $this->getTypeLabel($shipment),
|
||
];
|
||
|
||
$html = view('pdfs.awb', $data)->render();
|
||
|
||
return $this->generatePdf($html, 'A4', 'portrait');
|
||
}
|
||
|
||
/**
|
||
* تولید Invoice PDF — فقط برای PARCEL و DOC_NORMAL/DOC_ECONOMY
|
||
* (نه برای محمولههای DOC خالص که کالا ندارن)
|
||
*/
|
||
public function invoice(Shipment $shipment): string
|
||
{
|
||
// اگه محموله DOC هست و آیتم نداره، invoice تولید نکن
|
||
if ($shipment->type === ShipmentType::DocNormal || $shipment->type === ShipmentType::DocEconomy) {
|
||
if ($shipment->items->isEmpty()) {
|
||
throw new \InvalidArgumentException(
|
||
'فاکتور فقط برای محمولههای دارای کالا (PARCEL) صادر میشود. محمولههای DOC بدون کالا فاکتور ندارند.'
|
||
);
|
||
}
|
||
}
|
||
|
||
$shipment->loadMissing(['fromCountry', 'toCountry', 'items']);
|
||
|
||
$data = [
|
||
'shipment' => $shipment,
|
||
'shipper' => $this->formatAddress($shipment, 'sender'),
|
||
'receiver' => $this->formatAddress($shipment, 'receiver'),
|
||
'items' => $shipment->items ?? collect(),
|
||
'invoice_total_usd' => optional($shipment->items)->sum('total_usd') ?? 0,
|
||
'barcode_base64' => $this->generateBarcode($shipment->awb_no),
|
||
];
|
||
|
||
$html = view('pdfs.invoice', $data)->render();
|
||
|
||
return $this->generatePdf($html, 'A4', 'portrait');
|
||
}
|
||
|
||
/**
|
||
* تولید Label PDF — اندازه استاندارد لیبل پستی (100x150mm)
|
||
*/
|
||
public function label(Shipment $shipment): string
|
||
{
|
||
$shipment->loadMissing(['fromCountry', 'toCountry']);
|
||
|
||
$data = [
|
||
'shipment' => $shipment,
|
||
'barcode_base64' => $this->generateBarcode($shipment->awb_no),
|
||
'origin_iso' => $shipment->fromCountry?->iso_code ?? 'IR',
|
||
'dest_iso' => $shipment->toCountry?->iso_code ?? '',
|
||
];
|
||
|
||
$html = view('pdfs.label', $data)->render();
|
||
|
||
// A5 افقی (Landscape) — استاندارد لیبل پستی: 210×148 mm
|
||
return $this->generatePdf($html, 'A5', 'landscape');
|
||
}
|
||
|
||
/**
|
||
* تولید PDF فاکتور فروش خدمات (واردات) — مطابق Sheet ENG Invoice
|
||
*/
|
||
public function importInvoice(Shipment $shipment, array $options = []): string
|
||
{
|
||
$shipment->loadMissing(['fromCountry', 'toCountry', 'items']);
|
||
|
||
// آدرس گیرنده
|
||
$receiver = [
|
||
'name' => $shipment->receiver_name,
|
||
'company' => $shipment->receiver_company,
|
||
'phone' => $shipment->receiver_phone,
|
||
'email' => $shipment->receiver_email,
|
||
'address' => $shipment->receiver_address,
|
||
'city' => $shipment->receiver_city,
|
||
'country' => $shipment->toCountry?->name ?? '',
|
||
];
|
||
|
||
// شماره فاکتور (از شماره AWB مشتق میشود)
|
||
$invoice_no = $options['invoice_no'] ?? 'PEX' . date('ymd') . substr($shipment->awb_no, -4);
|
||
|
||
// تاریخ فاکتور
|
||
$invoice_date = $options['invoice_date'] ?? now()->format('l, F j, Y');
|
||
|
||
// دفتر گمرکی
|
||
$customs_office = $options['customs_office'] ?? 'Imam Khomeini Airport';
|
||
|
||
// نرخ ارز
|
||
$exchange_rate = $options['exchange_rate'] ?? 500000; // پیشفرض
|
||
|
||
// اقلام خدماتی (۱۰ ردیف ثابت مطابق Sheet ENG Invoice)
|
||
$service_items = $this->buildServiceItems($shipment, $options);
|
||
|
||
// جمعبندی
|
||
$freight_items = array_slice($service_items, 0, 5); // آیتمهای حمل
|
||
$clearance_items = array_slice($service_items, 5); // آیتمهای ترخیص
|
||
|
||
$freight_subtotal_currency = array_sum(array_column($freight_items, 'total_currency'));
|
||
$freight_subtotal_rial = array_sum(array_column($freight_items, 'total_rial'));
|
||
$clearance_subtotal_currency = array_sum(array_column($clearance_items, 'total_currency'));
|
||
$clearance_subtotal_rial = array_sum(array_column($clearance_items, 'total_rial'));
|
||
$total_currency = $freight_subtotal_currency + $clearance_subtotal_currency;
|
||
$total_rial = $freight_subtotal_rial + $clearance_subtotal_rial;
|
||
|
||
// شرایط و ضوابط
|
||
$terms_and_conditions = $options['terms_and_conditions'] ??
|
||
'This proforma invoice is issued based on the shipment weight declared by the customer. Final weight and dimensions will be determined after re-weighing at the destination customs.';
|
||
|
||
$data = [
|
||
'shipment' => $shipment,
|
||
'receiver' => $receiver,
|
||
'invoice_no' => $invoice_no,
|
||
'invoice_date' => $invoice_date,
|
||
'customs_office' => $customs_office,
|
||
'exchange_rate' => $exchange_rate,
|
||
'service_items' => $service_items,
|
||
'freight_subtotal_currency' => $freight_subtotal_currency,
|
||
'freight_subtotal_rial' => $freight_subtotal_rial,
|
||
'clearance_subtotal_currency' => $clearance_subtotal_currency,
|
||
'clearance_subtotal_rial' => $clearance_subtotal_rial,
|
||
'total_currency' => $total_currency,
|
||
'total_rial' => $total_rial,
|
||
'terms_and_conditions' => $terms_and_conditions,
|
||
];
|
||
|
||
$html = view('pdfs.import_invoice', $data)->render();
|
||
|
||
return $this->generatePdf($html, 'A4', 'portrait');
|
||
}
|
||
|
||
/**
|
||
* ساخت اقلام خدماتی فاکتور واردات
|
||
*/
|
||
private function buildServiceItems(Shipment $shipment, array $options): array
|
||
{
|
||
// فیلدهای پیشفرض از گزینهها یا مقادیر پیشفرض
|
||
$fields = [
|
||
'international_freight' => $options['international_freight'] ?? ($shipment->shipping_price ?? 0),
|
||
'pick_up_fee' => $options['pick_up_fee'] ?? ($shipment->domestic_pickup ?? 0),
|
||
'brand_fee' => $options['brand_fee'] ?? 0,
|
||
'report_fee' => $options['report_fee'] ?? 0,
|
||
'other_charges' => $options['other_charges'] ?? ($shipment->extra_service ?? 0),
|
||
'customs_clearance' => $options['customs_clearance'] ?? 0,
|
||
'domestic_transport' => $options['domestic_transport'] ?? ($shipment->domestic_delivery ?? 0),
|
||
'warehousing_fee' => $options['warehousing_fee'] ?? ($shipment->warehousing_cost ?? 0),
|
||
'order_registration_fee' => $options['order_registration_fee'] ?? 0,
|
||
'other_clearance_charges' => $options['other_clearance_charges'] ?? 0,
|
||
];
|
||
|
||
$exchange_rate = $options['exchange_rate'] ?? 500000;
|
||
$weight = $shipment->chargeable_weight ?? $shipment->weight;
|
||
|
||
return [
|
||
// آیتمهای حمل (Freight)
|
||
[
|
||
'description' => 'International Freight',
|
||
'unit' => 'kg',
|
||
'quantity' => $weight,
|
||
'unit_price' => $fields['international_freight'] > 0 ? $fields['international_freight'] / $weight : 0,
|
||
'total_currency' => $fields['international_freight'],
|
||
'total_rial' => $fields['international_freight'] * $exchange_rate,
|
||
'notes' => '',
|
||
],
|
||
[
|
||
'description' => 'Pick Up Fee',
|
||
'unit' => '',
|
||
'quantity' => 0,
|
||
'unit_price' => 0,
|
||
'total_currency' => $fields['pick_up_fee'],
|
||
'total_rial' => $fields['pick_up_fee'] * $exchange_rate,
|
||
'notes' => '',
|
||
],
|
||
[
|
||
'description' => 'Brand Fee',
|
||
'unit' => '',
|
||
'quantity' => 0,
|
||
'unit_price' => 0,
|
||
'total_currency' => $fields['brand_fee'],
|
||
'total_rial' => $fields['brand_fee'] * $exchange_rate,
|
||
'notes' => '',
|
||
],
|
||
[
|
||
'description' => 'Report Fee',
|
||
'unit' => '',
|
||
'quantity' => 0,
|
||
'unit_price' => 0,
|
||
'total_currency' => $fields['report_fee'],
|
||
'total_rial' => $fields['report_fee'] * $exchange_rate,
|
||
'notes' => '',
|
||
],
|
||
[
|
||
'description' => 'Other Charges',
|
||
'unit' => '',
|
||
'quantity' => 0,
|
||
'unit_price' => 0,
|
||
'total_currency' => $fields['other_charges'],
|
||
'total_rial' => $fields['other_charges'] * $exchange_rate,
|
||
'notes' => '',
|
||
],
|
||
// آیتمهای ترخیص (Clearance)
|
||
[
|
||
'description' => 'Customs Clearance',
|
||
'unit' => 'service',
|
||
'quantity' => $weight,
|
||
'unit_price' => $fields['customs_clearance'] > 0 ? $fields['customs_clearance'] / $weight : 0,
|
||
'total_currency' => $fields['customs_clearance'],
|
||
'total_rial' => $fields['customs_clearance'] * $exchange_rate,
|
||
'notes' => '',
|
||
],
|
||
[
|
||
'description' => 'Domestic Transport',
|
||
'unit' => '',
|
||
'quantity' => 0,
|
||
'unit_price' => 0,
|
||
'total_currency' => $fields['domestic_transport'],
|
||
'total_rial' => $fields['domestic_transport'] * $exchange_rate,
|
||
'notes' => '',
|
||
],
|
||
[
|
||
'description' => 'Warehousing Fee',
|
||
'unit' => '',
|
||
'quantity' => 0,
|
||
'unit_price' => 0,
|
||
'total_currency' => $fields['warehousing_fee'],
|
||
'total_rial' => $fields['warehousing_fee'] * $exchange_rate,
|
||
'notes' => '',
|
||
],
|
||
[
|
||
'description' => 'Order Registration Fee',
|
||
'unit' => '',
|
||
'quantity' => 0,
|
||
'unit_price' => 0,
|
||
'total_currency' => $fields['order_registration_fee'],
|
||
'total_rial' => $fields['order_registration_fee'] * $exchange_rate,
|
||
'notes' => '',
|
||
],
|
||
[
|
||
'description' => 'Other Clearance Charges',
|
||
'unit' => '',
|
||
'quantity' => 0,
|
||
'unit_price' => 0,
|
||
'total_currency' => $fields['other_clearance_charges'],
|
||
'total_rial' => $fields['other_clearance_charges'] * $exchange_rate,
|
||
'notes' => '',
|
||
],
|
||
];
|
||
}
|
||
|
||
/**
|
||
* تولید بارکد از AWB number — base64 embed (مطمئنترین روش)
|
||
*/
|
||
private function generateBarcode(string $awbNo): string
|
||
{
|
||
try {
|
||
$generator = new BarcodeGeneratorPNG();
|
||
$barcode = $generator->getBarcode($awbNo, $generator::TYPE_CODE_128, 2, 40);
|
||
|
||
// تبدیل به base64 و استفاده از data URI
|
||
return 'data:image/png;base64,' . base64_encode($barcode);
|
||
} catch (\Throwable $e) {
|
||
Log::error('Barcode generation failed', [
|
||
'awb' => $awbNo,
|
||
'error' => $e->getMessage(),
|
||
]);
|
||
return '';
|
||
}
|
||
}
|
||
|
||
/**
|
||
* برچسب Service برای AWB (Outbound/Inbound)
|
||
*/
|
||
private function getServiceLabel(Shipment $shipment): string
|
||
{
|
||
return match ($shipment->direction?->value) {
|
||
'export' => 'Outbound',
|
||
'import' => 'Inbound',
|
||
default => 'Outbound',
|
||
};
|
||
}
|
||
|
||
/**
|
||
* برچسب Type برای AWB (NON DOC / DOC)
|
||
*/
|
||
private function getTypeLabel(Shipment $shipment): string
|
||
{
|
||
return match ($shipment->type) {
|
||
ShipmentType::Parcel => 'NON DOC',
|
||
ShipmentType::DocNormal => 'DOC NORMAL',
|
||
ShipmentType::DocEconomy => 'DOC ECONOMY',
|
||
default => 'NON DOC',
|
||
};
|
||
}
|
||
|
||
/**
|
||
* تولید PDF از HTML
|
||
*/
|
||
private function generatePdf(string $html, array|string $paper, string $orientation): string
|
||
{
|
||
$options = new Options();
|
||
$options->set('isHtml5ParserEnabled', true);
|
||
$options->set('isRemoteEnabled', true); // مهم: برای data URI
|
||
$options->set('defaultFont', 'DejaVu Sans');
|
||
$options->set('dpi', 150);
|
||
$options->set('debugKeepTemp', false);
|
||
$options->set('debugCss', false);
|
||
$options->set('debugLayout', false);
|
||
|
||
$dompdf = new Dompdf($options);
|
||
$dompdf->loadHtml($html);
|
||
|
||
if (is_array($paper)) {
|
||
$dompdf->setPaper($paper, $orientation);
|
||
} else {
|
||
$dompdf->setPaper($paper, $orientation);
|
||
}
|
||
|
||
$dompdf->render();
|
||
|
||
return $dompdf->output();
|
||
}
|
||
|
||
/**
|
||
* فرمتبندی آدرس فرستنده/گیرنده
|
||
*/
|
||
private function formatAddress(Shipment $shipment, string $prefix): Collection
|
||
{
|
||
return collect([
|
||
'name' => $shipment->{$prefix . '_name'},
|
||
'company' => $shipment->{$prefix . '_company'},
|
||
'phone' => $shipment->{$prefix . '_phone'},
|
||
'email' => $shipment->{$prefix . '_email'},
|
||
'address' => $shipment->{$prefix . '_address'},
|
||
'city' => $shipment->{$prefix . '_city'},
|
||
'zip' => $shipment->{$prefix . '_zip'},
|
||
'id_number' => $shipment->{$prefix . '_id_number'},
|
||
]);
|
||
}
|
||
}
|