ifnex/04_Laravel/app/Services/PdfService.php
Kazem Alghasi 02c29db696 feat(core): implement order approval flow, credit system, and import invoicing
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.
2026-09-03 06:04:20 +03:30

368 lines
14 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?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'},
]);
}
}