ifnex/04_Laravel/app/Services/OrderPaymentService.php
Kazem Alghasi 8cf407553b feat(security): implement secure file storage and shipment access control
Introduce a 'secure' filesystem disk to prevent public access to sensitive
commitment form uploads. Files are now stored in a non-public directory
and served via a protected controller method that validates user ownership.

Additionally, implement shipment authorization policies to ensure users
can only access PDF documents (AWB, invoice, labels) belonging to their
own orders.

Other changes:
- Add production environment check for Zarinpal gateway configuration
  to prevent accidental use of sandbox credentials.
- Move discount code and commitment form routes under authentication
  middleware for improved security.
- Add `ShipmentPolicy` to handle resource authorization.
2026-10-01 03:34:28 +03:30

234 lines
9.2 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\PaymentGateway;
use App\Enums\ShipmentStatus;
use App\Enums\TransactionStatus;
use App\Enums\TransactionType;
use App\Models\Shipment;
use App\Models\ShipmentTrackingEvent;
use App\Models\Wallet;
use App\Models\WalletTransaction;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
class OrderPaymentService
{
/**
* پرداخت از کیف پول
*/
public function payFromWallet(Shipment $shipment, Wallet $wallet): array
{
// بررسی موجودی
if (!$wallet->hasSufficientBalance($shipment->total_fee)) {
return [
'success' => false,
'message' => 'موجودی کیف پول کافی نیست.',
'required' => $shipment->total_fee,
'available' => $wallet->balance,
];
}
try {
return DB::transaction(function () use ($shipment, $wallet) {
// ۱. ثبت تراکنش کسر از کیف پول
$transaction = WalletTransaction::create([
'wallet_id' => $wallet->id,
'amount' => -$shipment->total_fee,
'balance_before' => $wallet->balance,
'balance_after' => $wallet->balance - $shipment->total_fee,
'type' => TransactionType::ORDER_PAYMENT,
'status' => TransactionStatus::COMPLETED,
'gateway' => PaymentGateway::WALLET,
'description' => "پرداخت سفارش {$shipment->awb_no}",
'transactionable_type' => Shipment::class,
'transactionable_id' => $shipment->id,
'ip_address' => request()->ip(),
'user_agent' => request()->userAgent(),
]);
// ۲. به‌روزرسانی موجودی کیف پول
$wallet->update([
'balance' => $wallet->balance - $shipment->total_fee,
'total_withdrawn' => $wallet->total_withdrawn + $shipment->total_fee,
]);
// ۳. تغییر status سفارش به processed
$shipment->update(['status' => ShipmentStatus::Processed]);
// ۴. ثبت tracking event (با همه فیلدهای احتمالی)
$this->createTrackingEvent($shipment, 'پرداخت تکمیل شد (از کیف پول)');
Log::info('Order paid from wallet', [
'shipment_id' => $shipment->id,
'awb_no' => $shipment->awb_no,
'amount' => $shipment->total_fee,
'wallet_id' => $wallet->id,
]);
return [
'success' => true,
'message' => 'پرداخت با موفقیت انجام شد.',
'transaction_id' => $transaction->id,
'new_status' => 'processed',
];
});
} catch (\Exception $e) {
Log::error('Wallet payment failed', [
'shipment_id' => $shipment->id,
'error' => $e->getMessage(),
]);
return [
'success' => false,
'message' => 'خطا در پرداخت: ' . $e->getMessage(),
];
}
}
/**
* شروع پرداخت از درگاه
*/
public function initiateGatewayPayment(Shipment $shipment, Wallet $wallet): array
{
try {
// 🛡️ محافظ Production: اگه در محیط تولید، درگاه Mock فعال باشد، رد کن
if (app()->environment('production')
&& (config('ifnex.zarinpal.merchant_id') === 'fake-merchant-id-for-testing'
|| config('ifnex.zarinpal.sandbox', true))) {
throw new \RuntimeException(
'درگاه پرداخت در محیط تولید به‌درستی تنظیم نشده. '
. 'ZARINPAL_MERCHANT_ID واقعی یا ZARINPAL_SANDBOX=false در .env تنظیم کنید.'
);
}
// ۱. ایجاد تراکنش pending
$transaction = WalletTransaction::create([
'wallet_id' => $wallet->id,
'amount' => $shipment->total_fee,
'balance_before' => $wallet->balance,
'balance_after' => $wallet->balance,
'type' => TransactionType::ORDER_PAYMENT,
'status' => TransactionStatus::PENDING,
'gateway' => PaymentGateway::ZARINPAL,
'description' => "پرداخت سفارش {$shipment->awb_no}",
'transactionable_type' => Shipment::class,
'transactionable_id' => $shipment->id,
'ip_address' => request()->ip(),
'user_agent' => request()->userAgent(),
'metadata' => [
'shipment_id' => $shipment->id,
'awb_no' => $shipment->awb_no,
'is_order_payment' => true,
],
]);
// ۲. درخواست به درگاه
$gatewayService = app()->make(\App\Services\ZarinpalService::class);
if (config('ifnex.zarinpal.merchant_id') === 'fake-merchant-id-for-testing'
|| config('ifnex.zarinpal.sandbox', true)) {
$gatewayService = app()->make(\App\Services\MockZarinpalService::class);
}
$paymentData = $gatewayService->requestPayment(
amount: $shipment->total_fee,
description: "پرداخت سفارش {$shipment->awb_no}",
mobile: $shipment->user->phone,
email: $shipment->user->email
);
// ۳. ذخیره authority در metadata
$metadata = $transaction->metadata ?? [];
$metadata['zarinpal_authority'] = $paymentData['authority'];
$transaction->update(['metadata' => $metadata]);
return [
'success' => true,
'payment_url' => $paymentData['payment_url'],
'transaction_id' => $transaction->id,
'authority' => $paymentData['authority'],
];
} catch (\Exception $e) {
Log::error('Gateway payment initiation failed', [
'shipment_id' => $shipment->id,
'error' => $e->getMessage(),
]);
return [
'success' => false,
'message' => 'خطا در اتصال به درگاه: ' . $e->getMessage(),
];
}
}
/**
* تکمیل پرداخت بعد از callback موفق
*/
public function completeGatewayPayment(WalletTransaction $transaction): bool
{
try {
return DB::transaction(function () use ($transaction) {
$shipment = Shipment::find($transaction->transactionable_id);
if (!$shipment || $shipment->status !== ShipmentStatus::Approved) {
return false;
}
// ۱. به‌روزرسانی تراکنش
$transaction->update([
'status' => TransactionStatus::COMPLETED,
'balance_after' => $transaction->wallet->balance,
]);
// ۲. تغییر status سفارش
$shipment->update(['status' => ShipmentStatus::Processed]);
// ۳. ثبت tracking event
$this->createTrackingEvent($shipment, 'پرداخت تکمیل شد (از درگاه بانکی)');
Log::info('Order paid via gateway', [
'shipment_id' => $shipment->id,
'awb_no' => $shipment->awb_no,
'transaction_id' => $transaction->id,
]);
return true;
});
} catch (\Exception $e) {
Log::error('Complete gateway payment failed', [
'transaction_id' => $transaction->id,
'error' => $e->getMessage(),
]);
return false;
}
}
/**
* ساخت Tracking Event با ساختار دقیق جدول
*/
private function createTrackingEvent(Shipment $shipment, string $description): void
{
try {
ShipmentTrackingEvent::create([
'shipment_id' => $shipment->id,
'event_date' => now()->toDateString(), // YYYY-MM-DD
'event_time' => now()->format('H:i:s'), // HH:MM:SS ← فیلد جدید
'event_description' => $description,
'location' => 'سیستم',
'delivery_status' => 'processed', // ← اصلاح شد (status وجود نداشت)
'source' => 'manual', // مقدار معتبر enum TrackingSource
]);
} catch (\Exception $e) {
// اگر tracking event ثبت نشد، فقط لاگ کن (تراکنش را خراب نکن)
Log::warning('Failed to create tracking event', [
'shipment_id' => $shipment->id,
'error' => $e->getMessage(),
]);
}
}
}