feat(settlement): add receipt handling and detail view for settlements
This commit is contained in:
@@ -1495,6 +1495,52 @@ class AdminApiController extends BaseController
|
||||
return $this->paginated($items, (int) $total, $page, $limit);
|
||||
}
|
||||
|
||||
#[OA\Get(
|
||||
path: '/api/v1/admin/settlement/{uuid}',
|
||||
summary: 'جزئیات یک درخواست تسویه (ادمین)',
|
||||
security: [['bearerAuth' => []]],
|
||||
parameters: [new OA\Parameter(name: 'uuid', in: 'path', required: true, schema: new OA\Schema(type: 'string'))],
|
||||
responses: [
|
||||
new OA\Response(response: 200, description: 'جزئیات تسویه'),
|
||||
new OA\Response(response: 404, description: 'یافت نشد'),
|
||||
]
|
||||
)]
|
||||
#[Route('/api/v1/admin/settlement/{uuid}', methods: ['GET'])]
|
||||
public function settlementDetail(string $uuid): JsonResponse
|
||||
{
|
||||
$row = $this->em->createQueryBuilder()
|
||||
->select(
|
||||
's.uuid, s.amountRials, s.status, s.bankAccount, s.adminNote, s.receipt, s.reviewedAt, s.createdAt, s.updatedAt',
|
||||
'u.realName as user_name, u.mobileNumber as user_mobile',
|
||||
)
|
||||
->from(Settlement::class, 's')
|
||||
->join('s.user', 'u')
|
||||
->where('s.uuid = :uuid')
|
||||
->setParameter('uuid', $uuid)
|
||||
->getQuery()->getArrayResult();
|
||||
|
||||
if (empty($row)) {
|
||||
return $this->error('NOT_FOUND', 'درخواست تسویه یافت نشد', 404);
|
||||
}
|
||||
$s = $row[0];
|
||||
|
||||
return $this->success([
|
||||
'uuid' => $s['uuid'],
|
||||
'representation_name' => $s['user_name'] ?? $s['user_mobile'],
|
||||
'representation_mobile' => $s['user_mobile'],
|
||||
'amount' => (int) $s['amountRials'],
|
||||
'status' => $s['status'],
|
||||
'bank_card' => $s['bankAccount']['card'] ?? null,
|
||||
'bank_name' => $s['bankAccount']['bank_name'] ?? null,
|
||||
'bank_iban' => $s['bankAccount']['iban'] ?? null,
|
||||
'bank_owner' => $s['bankAccount']['owner_name'] ?? null,
|
||||
'reject_reason' => $s['adminNote'],
|
||||
'receipt' => $s['receipt'] ?? null,
|
||||
'requested_at' => date('c', (int) $s['createdAt']),
|
||||
'processed_at' => $s['reviewedAt'] ? date('c', (int) $s['reviewedAt']) : null,
|
||||
]);
|
||||
}
|
||||
|
||||
// ── SMS Templates (paginated list with optional status filter) ────────────
|
||||
|
||||
#[OA\Get(
|
||||
|
||||
@@ -15,6 +15,7 @@ use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
use Symfony\Component\Security\Http\Attribute\CurrentUser;
|
||||
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||
use Symfony\Component\Uid\Uuid;
|
||||
|
||||
#[OA\Tag(name: 'Settlements')]
|
||||
#[IsGranted('IS_AUTHENTICATED_FULLY')]
|
||||
@@ -23,6 +24,8 @@ class SettlementController extends BaseController
|
||||
public function __construct(
|
||||
private readonly SettlementRepository $settlementRepo,
|
||||
private readonly WalletTransactionRepository $walletRepo,
|
||||
private readonly \App\Shared\Service\FileValidatorService $fileValidator,
|
||||
private readonly string $projectDir,
|
||||
) {}
|
||||
|
||||
// ── Wallet ────────────────────────────────────────────────────────────────
|
||||
@@ -339,4 +342,97 @@ class SettlementController extends BaseController
|
||||
|
||||
return $this->success(['data' => $settlement->toArray()]);
|
||||
}
|
||||
|
||||
#[OA\Post(
|
||||
path: '/file/upload/clinic_pro/settlement/receipt',
|
||||
summary: 'آپلود رسید پرداخت تسویه (ROLE_ADMIN)',
|
||||
security: [['bearerAuth' => []]],
|
||||
responses: [new OA\Response(response: 200, description: 'فایل آپلود شد')]
|
||||
)]
|
||||
#[IsGranted('ROLE_ADMIN')]
|
||||
#[Route('/file/upload/clinic_pro/settlement/receipt', methods: ['POST'])]
|
||||
public function uploadReceipt(Request $request): JsonResponse
|
||||
{
|
||||
return $this->handleFileUpload($request, 'settlements/receipts');
|
||||
}
|
||||
|
||||
#[OA\Post(
|
||||
path: '/api/v1/settlement/{uuid}/paid',
|
||||
summary: 'ثبت پرداخت نهایی تسویه با رسید (ROLE_ADMIN)',
|
||||
security: [['bearerAuth' => []]],
|
||||
requestBody: new OA\RequestBody(
|
||||
required: true,
|
||||
content: new OA\JsonContent(
|
||||
required: ['receipt'],
|
||||
properties: [new OA\Property(property: 'receipt', type: 'string', description: 'URL رسید آپلودشده')]
|
||||
)
|
||||
),
|
||||
parameters: [new OA\Parameter(name: 'uuid', in: 'path', required: true, schema: new OA\Schema(type: 'string'))],
|
||||
responses: [
|
||||
new OA\Response(response: 200, description: 'پرداخت ثبت شد'),
|
||||
new OA\Response(response: 404, description: 'یافت نشد'),
|
||||
new OA\Response(response: 422, description: 'وضعیت نامعتبر یا رسید خالی'),
|
||||
]
|
||||
)]
|
||||
#[IsGranted('ROLE_ADMIN')]
|
||||
#[Route('/api/v1/settlement/{uuid}/paid', methods: ['POST'])]
|
||||
public function markPaid(string $uuid, Request $request): JsonResponse
|
||||
{
|
||||
$settlement = $this->settlementRepo->findByUuid($uuid);
|
||||
if ($settlement === null) {
|
||||
return $this->error(ErrorCodes::ERR_NOT_FOUND_001, 'درخواست یافت نشد', 404);
|
||||
}
|
||||
|
||||
// فقط درخواستِ تأییدشده قابل پرداخت است.
|
||||
if ($settlement->getStatus() !== Settlement::STATUS_APPROVED) {
|
||||
return $this->error(ErrorCodes::ERR_VALIDATION_001, 'فقط تسویهی تأییدشده قابل پرداخت است', 422);
|
||||
}
|
||||
|
||||
$data = json_decode($request->getContent(), true) ?? [];
|
||||
$receipt = trim((string) ($data['receipt'] ?? ''));
|
||||
if ($receipt === '') {
|
||||
return $this->error(ErrorCodes::ERR_VALIDATION_002, 'رسید پرداخت الزامی است', 422, 'receipt');
|
||||
}
|
||||
|
||||
// مبلغ هنگام ثبتِ درخواست از کیفپول کسر (reserve) شده؛ اینجا فقط نهاییسازی میشود.
|
||||
$settlement->markPaid($receipt);
|
||||
$this->settlementRepo->save($settlement);
|
||||
|
||||
return $this->success(['data' => $settlement->toArray()]);
|
||||
}
|
||||
|
||||
private function handleFileUpload(Request $request, string $subDir): JsonResponse
|
||||
{
|
||||
$content = $request->getContent();
|
||||
$disposition = $request->headers->get('Content-Disposition', '');
|
||||
preg_match('/filename=["\']?([^"\';\s]+)["\']?/i', $disposition, $m);
|
||||
$filename = $m[1] ?? 'receipt.jpg';
|
||||
|
||||
$tmpPath = sys_get_temp_dir() . '/' . uniqid('upload_', true);
|
||||
file_put_contents($tmpPath, $content);
|
||||
|
||||
try {
|
||||
$safeFilename = $this->fileValidator->sanitizeFilename($filename);
|
||||
$mime = $this->fileValidator->detectMimeType($tmpPath);
|
||||
|
||||
$year = date('Y'); $month = date('m');
|
||||
$dir = $this->projectDir . '/public/uploads/' . $subDir . '/' . $year . '-' . $month;
|
||||
if (!is_dir($dir)) mkdir($dir, 0755, true);
|
||||
|
||||
$storedName = uniqid('', true) . '_' . $safeFilename;
|
||||
rename($tmpPath, $dir . '/' . $storedName);
|
||||
|
||||
$url = '/uploads/' . $subDir . '/' . $year . '-' . $month . '/' . $storedName;
|
||||
|
||||
return $this->success([
|
||||
'uuid' => Uuid::v4()->toRfc4122(),
|
||||
'url' => $url,
|
||||
'filename' => $safeFilename,
|
||||
'filemime' => $mime,
|
||||
]);
|
||||
} catch (\Throwable $e) {
|
||||
if (file_exists($tmpPath)) unlink($tmpPath);
|
||||
return $this->error(ErrorCodes::ERR_VALIDATION_001, $e->getMessage(), 422);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,6 +40,9 @@ class Settlement
|
||||
#[ORM\Column(name: 'admin_note', type: 'string', length: 500, nullable: true)]
|
||||
private ?string $adminNote = null;
|
||||
|
||||
#[ORM\Column(name: 'receipt', type: 'string', length: 500, nullable: true)]
|
||||
private ?string $receipt = null;
|
||||
|
||||
#[ORM\Column(name: 'reviewed_by', type: 'integer', nullable: true)]
|
||||
private ?int $reviewedBy = null;
|
||||
|
||||
@@ -69,6 +72,7 @@ class Settlement
|
||||
public function getStatus(): string { return $this->status; }
|
||||
public function getBankAccount(): ?array { return $this->bankAccount; }
|
||||
public function getAdminNote(): ?string { return $this->adminNote; }
|
||||
public function getReceipt(): ?string { return $this->receipt; }
|
||||
|
||||
public function approve(int $adminUserId, ?string $note = null): self
|
||||
{
|
||||
@@ -90,9 +94,10 @@ class Settlement
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function markPaid(): self
|
||||
public function markPaid(?string $receipt = null): self
|
||||
{
|
||||
$this->status = self::STATUS_PAID;
|
||||
if ($receipt !== null) $this->receipt = $receipt;
|
||||
$this->updatedAt = time();
|
||||
return $this;
|
||||
}
|
||||
@@ -105,6 +110,7 @@ class Settlement
|
||||
'status' => $this->status,
|
||||
'bank_account' => $this->bankAccount,
|
||||
'admin_note' => $this->adminNote,
|
||||
'receipt' => $this->receipt,
|
||||
'reviewed_at' => $this->reviewedAt,
|
||||
'created_at' => $this->createdAt,
|
||||
];
|
||||
|
||||
Reference in New Issue
Block a user