feat(accounting): add Shayegan as fixed boursecu provider

Wire outbound Invoice/Document sync after confirmed ops, store sync status with timeout lock, and keep SQL credentials out of frontend responses.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-10-05 17:12:43 +03:30
co-authored by Cursor
parent 698fbed805
commit a42261c06e
20 changed files with 1485 additions and 18 deletions
@@ -81,7 +81,7 @@ class SettingsController extends Controller
->orderBy('key')
->get();
return response()->json($rows);
return response()->json($this->maskSecretSettingValues($rows));
}
/**
@@ -162,7 +162,19 @@ class SettingsController extends Controller
$previousValue = $setting->exists ? (string) ($setting->value ?? '') : null;
$setting->value = $request->value;
$incomingValue = $request->value;
if (
in_array($key, ['shayegan_db_password', 'shayegan_auth_password'], true)
&& is_string($incomingValue)
&& ($incomingValue === '********' || $incomingValue === '')
&& $previousValue !== null
&& $previousValue !== ''
) {
// ماسک frontend را به‌جای رمز واقعی ذخیره نکن
$incomingValue = $previousValue;
}
$setting->value = $incomingValue;
$setting->description = $request->description;
if ($request->has('is_active')) {
$setting->is_active = filter_var($request->is_active, FILTER_VALIDATE_BOOLEAN) ? 1 : 0;
@@ -327,7 +339,7 @@ class SettingsController extends Controller
$overlay = app(SuperAdminProgramSettingsOverlay::class);
$saMap = $overlay->catalogMap();
if ($saMap === null) {
return $localRows;
return $this->maskSecretSettingValues($localRows);
}
$localByKey = [];
@@ -380,7 +392,53 @@ class SettingsController extends Controller
}
}
return collect(array_values($byKey))->sortBy(fn ($row) => (string) ($row->key ?? ''))->values();
return $this->maskSecretSettingValues(
collect(array_values($byKey))->sortBy(fn ($row) => (string) ($row->key ?? ''))->values()
);
}
/**
* رمزهای شایگان و مشابه در پاسخ frontend برنگردند (فقط پر/خالی بودن).
*
* @param mixed $rows
* @return mixed
*/
private function maskSecretSettingValues($rows)
{
$secretKeys = [
'shayegan_db_password',
'shayegan_auth_password',
];
$maskOne = function ($row) use ($secretKeys) {
if (is_array($row)) {
$key = (string) ($row['key'] ?? '');
if (in_array($key, $secretKeys, true)) {
$val = (string) ($row['value'] ?? '');
$row['value'] = $val !== '' ? '********' : '';
}
return $row;
}
if (is_object($row) && isset($row->key)) {
$key = (string) $row->key;
if (in_array($key, $secretKeys, true)) {
$val = (string) ($row->value ?? '');
$row->value = $val !== '' ? '********' : '';
}
}
return $row;
};
if ($rows instanceof \Illuminate\Support\Collection) {
return $rows->map($maskOne)->values();
}
if (is_array($rows)) {
return array_map($maskOne, $rows);
}
return $rows;
}
private function isEditableCanEdit(mixed $canEdit): bool
@@ -0,0 +1,171 @@
<?php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\Models\Shayegan;
use App\Services\ShayeganClient;
use App\Services\ShayeganSyncService;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Schema;
class ShayeganController extends Controller
{
/** @var ShayeganSyncService */
private $sync;
/** @var ShayeganClient */
private $client;
public function __construct(ShayeganSyncService $sync, ShayeganClient $client)
{
$this->sync = $sync;
$this->client = $client;
}
public function healthCheck()
{
$result = $this->client->healthCheck();
$status = ! empty($result['ok']) ? 200 : 422;
return response()->json([
'ok' => ! empty($result['ok']),
'message' => $result['message'] ?? null,
'http_status' => $result['http_status'] ?? null,
'timeout' => ! empty($result['timeout']),
// never echo Config passwords
'body' => $this->sanitizeBodyForAdmin($result['body'] ?? null),
], $status);
}
/**
* @param array<string, mixed> $data
*/
public function storeTrade(array $data)
{
return $this->sync->recordTrade($data);
}
/**
* @param array<string, mixed> $data
*/
public function wallet(array $data)
{
return $this->sync->recordWalletCash($data);
}
/**
* @param array<string, mixed> $data
*/
public function walletGold(array $data)
{
return $this->sync->recordWalletGold($data);
}
public function table(Request $request)
{
if (! Schema::hasTable('shayegan')) {
return response()->json(['data' => [], 'total' => 0]);
}
$q = Shayegan::query()->orderByDesc('id');
if ($request->filled('sync_status')) {
$q->where('sync_status', $request->get('sync_status'));
}
if ($request->filled('type')) {
$q->where('type', $request->get('type'));
}
$perPage = min(100, max(10, (int) $request->get('per_page', 20)));
$page = $q->paginate($perPage);
return response()->json([
'data' => $page->items(),
'total' => $page->total(),
'current_page' => $page->currentPage(),
'per_page' => $page->perPage(),
]);
}
public function show($id)
{
$row = Shayegan::findOrFail($id);
return response()->json($row);
}
/**
* Manual retry — refuses timeout rows unless force=1 after operator verified remote.
*/
public function retrySanad(Request $request, $id)
{
$row = Shayegan::findOrFail($id);
if ($row->sync_status === ShayeganSyncService::STATUS_SUCCESS) {
return response()->json(['ok' => true, 'message' => 'already synced', 'row' => $row]);
}
if ($row->sync_status === ShayeganSyncService::STATUS_TIMEOUT && ! $request->boolean('force')) {
return response()->json([
'ok' => false,
'message' => 'این سند timeout شده؛ بدون بررسی نتیجه در حسابگر دوباره ارسال نکنید. در صورت تایید force=1 بفرستید.',
], 422);
}
$payload = json_decode((string) $row->data, true);
$put = is_array($payload) ? ($payload['PutObject'] ?? null) : null;
$endpoint = (string) ($row->endpoint ?: 'Document/Put');
if (! is_array($put) || $put === []) {
return response()->json(['ok' => false, 'message' => 'payload خالی است'], 422);
}
// Clear timeout lock only when force retry requested
if ($row->sync_status === ShayeganSyncService::STATUS_TIMEOUT) {
$row->sync_status = ShayeganSyncService::STATUS_PENDING;
$row->save();
}
$result = $this->client->put($endpoint, $put);
if (! empty($result['timeout'])) {
$row->sync_status = ShayeganSyncService::STATUS_TIMEOUT;
$row->sync_log = (string) ($result['message'] ?? 'timeout');
$row->res = json_encode(['timeout' => true], JSON_UNESCAPED_UNICODE);
$row->save();
return response()->json(['ok' => false, 'message' => 'timeout', 'row' => $row], 422);
}
if (! empty($result['ok'])) {
$row->sync_status = ShayeganSyncService::STATUS_SUCCESS;
$row->sync_log = null;
$row->shayegan_number = $result['number'] ?? null;
$row->shayegan_guid = $result['guid'] ?? null;
$row->res = json_encode($result['body'] ?? [], JSON_UNESCAPED_UNICODE);
$row->save();
return response()->json(['ok' => true, 'row' => $row]);
}
$row->sync_status = ShayeganSyncService::STATUS_FAILED;
$row->sync_log = (string) ($result['message'] ?? 'failed');
$row->res = json_encode($result['body'] ?? [], JSON_UNESCAPED_UNICODE);
$row->save();
return response()->json(['ok' => false, 'message' => $row->sync_log, 'row' => $row], 422);
}
/**
* @param mixed $body
* @return mixed
*/
private function sanitizeBodyForAdmin($body)
{
if (! is_array($body)) {
return $body;
}
// Drop huge Result lists from health if present
if (isset($body['Result']) && is_array($body['Result'])) {
$body['Result'] = array_slice($body['Result'], 0, 3);
}
return $body;
}
}
@@ -284,6 +284,16 @@ class WalletController extends Controller
}
}
if (app(ConfigService::class)->isShayeganAccounting()) {
try {
$data['direction'] = $deposit ? 0 : 1;
$data['withdraw_id'] = $Withdraw->id;
app(\App\Http\Controllers\Admin\ShayeganController::class)->wallet($data);
} catch (\Exception $e) {
report($e);
}
}
try {
app(PargarSyncService::class)->queueWalletDocument(
$customer,
@@ -430,6 +440,16 @@ class WalletController extends Controller
}
}
if (app(ConfigService::class)->isShayeganAccounting()) {
try {
$data['direction'] = $deposit ? 1 : 0;
$data['withdraw_id'] = $Withdraw->id;
app(\App\Http\Controllers\Admin\ShayeganController::class)->walletGold($data);
} catch (\Exception $e) {
report($e);
}
}
try {
app(PargarSyncService::class)->queueWalletDocument(
$customer,
@@ -669,6 +689,17 @@ class WalletController extends Controller
}
}
if (app(ConfigService::class)->isShayeganAccounting()) {
try {
$data['direction'] = $deposit ? 1 : 0;
$data['withdraw_id'] = $Withdraw->id;
$data['product_id'] = $productId;
app(\App\Http\Controllers\Admin\ShayeganController::class)->walletGold($data);
} catch (\Exception $e) {
report($e);
}
}
try {
// فقط محصول متصل به کیف اصلی → سند طلای پرگار
app(PargarSyncService::class)->queueWalletDocument(