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
@@ -0,0 +1,39 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
if (! Schema::hasTable('shayegan')) {
Schema::create('shayegan', function (Blueprint $table) {
$table->id();
$table->unsignedBigInteger('customer_id')->nullable()->index();
$table->unsignedBigInteger('order_id')->nullable()->index();
$table->unsignedBigInteger('withdraw_id')->nullable()->index();
$table->decimal('amount', 18, 4)->nullable();
$table->string('desc')->nullable();
$table->string('type', 32)->nullable()->index();
$table->timestamp('date')->nullable();
$table->string('endpoint', 64)->nullable();
$table->string('request_guid', 64)->nullable()->index();
$table->string('shayegan_number')->nullable()->index();
$table->string('shayegan_guid')->nullable()->index();
$table->longText('data')->nullable();
$table->longText('res')->nullable();
// pending|success|failed|timeout — timeout must not auto-resend
$table->string('sync_status', 20)->default('pending')->index();
$table->text('sync_log')->nullable();
$table->timestamps();
});
}
}
public function down(): void
{
Schema::dropIfExists('shayegan');
}
};
@@ -0,0 +1,168 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
/**
* تنظیمات اتصال شایگان — value ردیف موجود را UPDATE نمی‌کند.
* رمزها را در Git نگذار؛ بعد از migrate در DB پر کن.
*/
return new class extends Migration
{
private const KEYS = [
'shayegan_base_url' => [
'value' => 'http://2.180.4.172:2030',
'title_fa' => 'آدرس پایه وب‌سرویس شایگان',
'description' => 'بدون /api در انتها. عمومی دفتر: http://2.180.4.172:2030 — داخلی LAN: http://192.168.109.222:2030 — اگر 404 شد /WsServer اضافه کنید. نام SQL را اینجا نگذارید.',
'type' => 'text',
],
'shayegan_sql_server' => [
'value' => 'P-SERVER',
'title_fa' => 'نام SQL Server شایگان (Config.Server)',
'description' => 'Config.Server — فقط برای بدنهٔ وب‌سرویس، نه آدرس HTTP. (نمونهٔ بورس مس: P-SERVER)',
'type' => 'text',
],
'shayegan_database' => [
'value' => 'CYPRSD04',
'title_fa' => 'نام دیتابیس حسابگر (Config.DataBaseName)',
'description' => 'نمونهٔ عملیاتی بورس مس: CYPRSD04 — برای تست ثبت از دیتابیس آزمایشی استفاده کنید.',
'type' => 'text',
],
'shayegan_db_user' => [
'value' => 'sa',
'title_fa' => 'کاربر SQL Server شایگان',
'description' => 'Config.DBUserName — فقط backend؛ در پاسخ API ماسک می‌شود.',
'type' => 'text',
],
'shayegan_db_password' => [
'value' => '',
'title_fa' => 'رمز SQL Server شایگان',
'description' => 'Config.DBPassword — فقط backend؛ در لاگ و frontend برنمی‌گردد.',
'type' => 'password',
],
'shayegan_auth_user' => [
'value' => '',
'title_fa' => 'AuthUser وب‌سرویس شایگان',
'description' => 'خالی = بدون تایید هویت، یا کاربر حسابگر/شایگان طبق حالت نصب.',
'type' => 'text',
],
'shayegan_auth_password' => [
'value' => '',
'title_fa' => 'AuthPassword وب‌سرویس شایگان',
'description' => 'فقط backend؛ در لاگ و frontend برنمی‌گردد.',
'type' => 'password',
],
'shayegan_language' => [
'value' => '3',
'title_fa' => 'Language شایگان',
'description' => 'Config.Language — نمونه مستندات معمولاً 3 است.',
'type' => 'number',
],
'shayegan_timeout_seconds' => [
'value' => '30',
'title_fa' => 'Timeout HTTP شایگان (ثانیه)',
'description' => 'پس از timeout وضعیت timeout ذخیره می‌شود و ارسال خودکار تکرار نمی‌شود.',
'type' => 'number',
],
'shayegan_item_copper' => [
'value' => '',
'title_fa' => 'کد کالای مس در حسابگر (ItemNumber)',
'description' => 'برای فاکتور خرید/فروش مس.',
'type' => 'text',
],
'shayegan_stock_number' => [
'value' => '',
'title_fa' => 'شماره انبار مس (STNumber)',
'description' => 'انبار ردیف فاکتور خرید/فروش.',
'type' => 'text',
],
'shayegan_bank_account' => [
'value' => '',
'title_fa' => 'شماره حساب بانک/صندوق ریالی',
'description' => 'طرف حساب بانک در Document واریز/برداشت ریالی.',
'type' => 'text',
],
'shayegan_moin_customer' => [
'value' => '',
'title_fa' => 'شماره معین مشتریان (MoinNumber)',
'description' => 'برای Account/Put هنگام ساخت حساب مشتری در حسابگر.',
'type' => 'text',
],
'shayegan_amount_scale' => [
'value' => '1',
'title_fa' => 'ضریب مبلغ به شایگان',
'description' => 'مبلغ پنل ریال است. اگر حسابگر تومان است 0.1 بگذارید؛ اگر ریال است 1.',
'type' => 'text',
],
'shayegan_weight_scale' => [
'value' => '1',
'title_fa' => 'ضریب وزن به Quan شایگان',
'description' => 'وزن پنل را به واحد کالای حسابگر تبدیل می‌کند (مثلاً گرم→کیلو = 0.001).',
'type' => 'text',
],
'shayegan_metal_wallet_mode' => [
'value' => 'skip',
'title_fa' => 'حالت واریز/برداشت محصول به شایگان',
'description' => 'skip=ارسال نشود | document=فقط سند حسابداری | receipt_havale=رسید(5)/حواله(4). تغییر کیف را ورود/خروج انبار فرض نکنید مگر صریح.',
'type' => 'text',
],
];
public function up(): void
{
$hasMenu = Schema::hasColumn('settings', 'menu');
foreach (self::KEYS as $key => $meta) {
$exists = DB::table('settings')->where('key', $key)->exists();
if (! $exists) {
$row = [
'key' => $key,
'value' => $meta['value'],
'title_fa' => $meta['title_fa'],
'can_edit' => 0,
'is_active' => 1,
'description' => $meta['description'],
'type' => $meta['type'],
];
if ($hasMenu) {
$row['menu'] = 'accounting_shayegan';
}
DB::table('settings')->insert($row);
continue;
}
$update = [
'title_fa' => $meta['title_fa'],
'description' => $meta['description'],
'type' => $meta['type'],
'can_edit' => 0,
'is_active' => 1,
];
if ($hasMenu) {
$update['menu'] = 'accounting_shayegan';
}
DB::table('settings')->where('key', $key)->update($update);
}
$provider = DB::table('settings')->where('key', 'accounting_provider')->first();
if ($provider) {
$desc = (string) ($provider->description ?? '');
if (strpos($desc, 'shayegan') === false) {
if (strpos($desc, 'none,pargar,tahesab,zarin') !== false) {
$newDesc = str_replace('none,pargar,tahesab,zarin', 'none,pargar,tahesab,zarin,shayegan', $desc);
} else {
$newDesc = trim($desc) === '' ? 'none,pargar,tahesab,zarin,shayegan' : ($desc . ',shayegan');
}
DB::table('settings')->where('key', 'accounting_provider')->update([
'description' => $newDesc,
]);
}
}
}
public function down(): void
{
DB::table('settings')->whereIn('key', array_keys(self::KEYS))->delete();
}
};
@@ -0,0 +1,44 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Support\Facades\DB;
/**
* بورس مس: مقدار accounting_provider فقط وقتی خالی/none است → shayegan (valueهای دیگر دست نمی‌خورد).
*/
return new class extends Migration
{
public function up(): void
{
if (! DB::getSchemaBuilder()->hasTable('settings')) {
return;
}
DB::table('settings')
->where('key', 'accounting_provider')
->where(function ($q) {
$q->whereNull('value')
->orWhere('value', '')
->orWhere('value', 'none');
})
->update(['value' => 'shayegan']);
$row = DB::table('settings')->where('key', 'accounting_provider')->first();
if (! $row) {
DB::table('settings')->insert([
'key' => 'accounting_provider',
'value' => 'shayegan',
'title_fa' => 'سیستم حسابداری',
'can_edit' => 0,
'is_active' => 1,
'description' => 'boursecu: shayegan (ثابت در کد)',
'type' => 'select',
]);
}
}
public function down(): void
{
// عمداً برنمی‌گردانیم — بورس مس همیشه شایگان است.
}
};