ifnex/04_Laravel/app/Http/Controllers/Api/CommitmentFormController.php
Kazem Alghasi b9ede16613 refactor(core): harden production environment and fix discount logic
Implement critical security hardening and resolve a P0 bug in the
discount code consumption flow. This update transitions the system
from a development-heavy configuration to a production-ready state.

Key changes:
- Fix P0 bug where `consumeDiscountCode()` was missing, breaking order
  registration; replaced with a robust `DiscountCodeService` using
  `discount_code_usages` for audit trails and idempotency.
- Implement strict production guards for the Zarinpal payment gateway
  to prevent accidental mock processing in live environments.
- Refactor WordPress bridge asset management to use `filemtime` based
  versioning instead of `time()` to prevent unnecessary cache busting.
- Remove debug-only hacks (`opcache_reset`, `header` cache control)
  from the WordPress plugin.
- Enhance file upload security by enforcing strict `mimetypes` validation.
- Update `.gitignore` to exclude sensitive WordPress configuration
  files and local PHPUnit caches.
- Add comprehensive documentation for production deployment, including
  security checklists and key rotation procedures.
- Update test suite to reflect new architecture and ensure 100%
  coverage of the discount service.
2026-10-31 19:18:58 +03:30

272 lines
10 KiB
PHP

<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Models\CommitmentForm;
use App\Models\Shipment;
use App\Models\ShipmentCommitmentForm;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Storage;
class CommitmentFormController extends Controller
{
/**
* دریافت لیست فایل‌های تعهدنامه فعال
* GET /api/v1/commitment-forms
*/
public function index(): JsonResponse
{
$forms = CommitmentForm::query()
->where('is_active', true)
->orderBy('sort_order')
->orderBy('created_at', 'desc')
->get()
->map(fn ($form) => [
'id' => $form->id,
'title' => $form->title,
'description' => $form->description,
'file_url' => $form->file_url,
'file_type' => strtoupper(pathinfo($form->file_path, PATHINFO_EXTENSION)),
'direction' => $form->direction,
]);
return response()->json([
'success' => true,
'data' => $forms,
]);
}
/**
* دریافت تعهدنامه‌ها بر اساس جهت ارسال
* GET /api/v1/commitment-forms/{direction}
*/
public function byDirection(string $direction): JsonResponse
{
$forms = CommitmentForm::query()
->where('is_active', true)
->where(function ($query) use ($direction) {
$query->where('direction', 'both')
->orWhere('direction', $direction);
})
->orderBy('sort_order')
->orderBy('created_at', 'desc')
->get()
->map(fn ($form) => [
'id' => $form->id,
'title' => $form->title,
'description' => $form->description,
'file_url' => $form->file_url,
'file_type' => strtoupper(pathinfo($form->file_path, PATHINFO_EXTENSION)),
'direction' => $form->direction,
]);
return response()->json([
'success' => true,
'data' => $forms,
]);
}
/**
* دریافت لیست تعهدنامه‌های مربوط به سفارش خاص
* GET /api/v1/customer/orders/{shipment}/commitment-forms
*/
public function shipmentForms(Shipment $shipment): JsonResponse
{
$user = auth()->user();
// بررسی مالکیت
if ($shipment->user_id !== $user->id) {
return response()->json([
'success' => false,
'message' => 'شما به این سفارش دسترسی ندارید.',
], 403);
}
$forms = CommitmentForm::query()
->where('is_active', true)
->where(function ($query) use ($shipment) {
$query->where('direction', 'both')
->orWhere('direction', $shipment->direction);
})
->orderBy('sort_order')
->orderBy('created_at', 'desc')
->get();
$rows = ShipmentCommitmentForm::where('shipment_id', $shipment->id)
->whereIn('commitment_form_id', $forms->pluck('id'))
->get()
->keyBy('commitment_form_id');
// ساخت ردیف pending برای فرم‌هایی که هنوز رکورد ندارند —
// تا بخش تعهدنامه‌ها در پنل ادمین همه فرم‌ها را همراه وضعیت نمایش دهد
foreach ($forms as $form) {
if (!isset($rows[$form->id])) {
$rows[$form->id] = ShipmentCommitmentForm::create([
'shipment_id' => $shipment->id,
'commitment_form_id' => $form->id,
'status' => 'pending',
]);
}
}
$data = $forms->map(function ($form) use ($shipment, $rows) {
$upload = $rows[$form->id];
return [
'id' => $form->id,
'title' => $form->title,
'description' => $form->description,
// 🛡️ URL دانلود قالب از طریق route محافظت‌شده (نه asset عمومی)
'file_url' => route('customer.commitment-forms.template', [
'shipment' => $shipment->id,
'form' => $form->id,
]),
'file_type' => strtoupper(pathinfo($form->file_path, PATHINFO_EXTENSION)),
'direction' => $form->direction,
'upload_status' => $upload->status,
'uploaded_file_url' => $upload->uploaded_file_path
? route('customer.commitment-forms.download', ['shipment' => $shipment->id, 'form' => $form->id])
: null,
'uploaded_at' => $upload->created_at,
'notes' => $upload->notes,
];
});
return response()->json([
'success' => true,
'data' => $data,
]);
}
/**
* دانلود قالب خام تعهدنامه (محافظت‌شده با بررسی مالکیت سفارش).
* GET /api/v1/customer/orders/{shipment}/commitment-forms/{form}/template
*/
public function downloadTemplate(Shipment $shipment, CommitmentForm $form)
{
$user = auth()->user();
if ($shipment->user_id !== $user->id) {
return response()->json([
'success' => false,
'message' => 'شما به این سفارش دسترسی ندارید.',
], 403);
}
if (!$form->file_path || !Storage::disk('public')->exists($form->file_path)) {
return response()->json([
'success' => false,
'message' => 'فایل قالب یافت نشد.',
], 404);
}
$ext = pathinfo($form->file_path, PATHINFO_EXTENSION);
$filename = 'template-' . $form->id . '-' . $shipment->awb_no . '.' . $ext;
return Storage::disk('public')->download($form->file_path, $filename);
}
/**
* آپلود فرم تعهدنامه امضاشده
* POST /api/v1/customer/orders/{shipment}/commitment-forms/{form}/upload
*/
public function uploadSigned(Request $request, Shipment $shipment, CommitmentForm $form): JsonResponse
{
$user = auth()->user();
// بررسی مالکیت
if ($shipment->user_id !== $user->id) {
return response()->json([
'success' => false,
'message' => 'شما به این سفارش دسترسی ندارید.',
], 403);
}
$request->validate([
// سخت‌سازی Production: mimetypes محتوا را واقعاً بررسی می‌کند (نه فقط پسوند)
'file' => [
'required',
'file',
'mimes:pdf,jpg,jpeg,png',
'mimetypes:application/pdf,image/jpeg,image/png',
'max:5120', // حداکثر 5MB
],
'notes' => 'nullable|string|max:1000',
]);
try {
$file = $request->file('file');
// 🛡️ ذخیره در دیسک امن (غیرعمومی) — فقط از طریق route محافظت‌شده قابل دانلود
$path = $file->store("commitment-forms/{$shipment->id}", 'secure');
$upload = ShipmentCommitmentForm::updateOrCreate(
[
'shipment_id' => $shipment->id,
'commitment_form_id' => $form->id,
],
[
'uploaded_file_path' => $path,
'uploaded_file_type' => $file->getClientOriginalExtension(),
'uploaded_file_size' => $file->getSize(),
'status' => 'uploaded',
'notes' => $request->input('notes'),
'uploaded_by' => $user->id,
]
);
return response()->json([
'success' => true,
'message' => 'فایل با موفقیت آپلود شد.',
'data' => [
'id' => $upload->id,
'file_url' => route('customer.commitment-forms.download', [
'shipment' => $shipment->id,
'form' => $form->id,
]),
'file_type' => $upload->uploaded_file_type,
'status' => $upload->status,
'uploaded_at' => $upload->created_at,
],
]);
} catch (\Exception $e) {
return response()->json([
'success' => false,
'message' => 'خطا در آپلود فایل: ' . $e->getMessage(),
], 500);
}
}
/**
* دانلود فرم تعهدنامه امزاشده (محافظت‌شده با بررسی مالکیت).
* GET /api/v1/customer/orders/{shipment}/commitment-forms/{form}/download
*/
public function downloadSigned(Shipment $shipment, CommitmentForm $form)
{
$user = auth()->user();
if ($shipment->user_id !== $user->id) {
return response()->json([
'success' => false,
'message' => 'شما به این سفارش دسترسی ندارید.',
], 403);
}
$upload = ShipmentCommitmentForm::where('shipment_id', $shipment->id)
->where('commitment_form_id', $form->id)
->firstOrFail();
if (!$upload->uploaded_file_path) {
return response()->json([
'success' => false,
'message' => 'فایلی برای این تعهدنامه آپلود نشده است.',
], 404);
}
return Storage::disk('secure')->download(
$upload->uploaded_file_path,
'commitment-' . $form->id . '-' . $shipment->awb_no . '.' . $upload->uploaded_file_type
);
}
}