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:
@@ -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(
|
||||
|
||||
@@ -445,6 +445,14 @@ class CartController extends Controller
|
||||
report($e);
|
||||
}
|
||||
}
|
||||
|
||||
if ($config->isShayeganAccounting()) {
|
||||
try {
|
||||
app(\App\Http\Controllers\Admin\ShayeganController::class)->storeTrade($payload);
|
||||
} catch (\Throwable $e) {
|
||||
report($e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($config->isPargarAccounting()) {
|
||||
|
||||
@@ -486,6 +486,20 @@ class WalletController extends Controller
|
||||
}
|
||||
}
|
||||
|
||||
if (app(ConfigService::class)->isShayeganAccounting()) {
|
||||
try {
|
||||
app(\App\Http\Controllers\Admin\ShayeganController::class)->wallet([
|
||||
'customer_id' => auth('customer')->id(),
|
||||
'value' => $amount,
|
||||
'direction' => 1,
|
||||
'withdraw_id' => $Withdraw->id,
|
||||
'note' => 'برداشت ریالی',
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
report($e);
|
||||
}
|
||||
}
|
||||
|
||||
$companyName = $this->Config('name_fa'); // Company name from configuration
|
||||
$message = "کاربر $userName با شماره موبایل $userMobile درخواست برداشت مبلغ " . number_format($withdrawAmount) . " ریال را دارد. با تشکر، $companyName";
|
||||
User::where('send_sms', 1)->get()->each(function ($admin) use ($message) {
|
||||
|
||||
@@ -2362,6 +2362,14 @@ class SingleOrderController extends Controller
|
||||
}
|
||||
}
|
||||
|
||||
if ($config->isShayeganAccounting()) {
|
||||
try {
|
||||
$data['shayegan'] = app(\App\Http\Controllers\Admin\ShayeganController::class)->storeTrade($data);
|
||||
} catch (\Throwable $e) {
|
||||
report($e);
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($data['order'])) {
|
||||
try {
|
||||
app(\App\Services\ColleagueAccountingService::class)->recordCoveredTrade($data);
|
||||
|
||||
Reference in New Issue
Block a user