Add gold credit feature gated by SuperAdmin enable_gold_credit.

Port tables, APIs, and overdue cron from goldali, default the flag off, and keep customer routes behind auth.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-08-30 00:17:01 +03:30
co-authored by Cursor
parent a8be3d499b
commit 050bd26318
18 changed files with 1670 additions and 5 deletions
@@ -0,0 +1,79 @@
<?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
{
Schema::create('gold_credits', function (Blueprint $table) {
$table->id();
$table->unsignedBigInteger('customer_id');
$table->decimal('gold_amount', 12, 4);
$table->unsignedSmallInteger('months');
$table->enum('interest_type', ['none', 'percent', 'fixed_gram'])->default('none');
$table->decimal('interest_value', 12, 4)->default(0);
$table->enum('delivery_method', ['auto', 'manual', 'in_person'])->default('manual');
$table->enum('status', ['pending', 'active', 'completed', 'cancelled'])->default('active');
$table->enum('source', ['admin_direct', 'user_request'])->default('admin_direct');
$table->decimal('total_interest_gold', 12, 4)->default(0);
$table->decimal('total_payable_gold', 12, 4)->default(0);
$table->decimal('paid_gold', 12, 4)->default(0);
$table->unsignedBigInteger('paid_rial')->default(0);
$table->unsignedBigInteger('created_by')->nullable();
$table->text('notes')->nullable();
$table->timestamps();
$table->foreign('customer_id', 'gc_credit_customer_fk')
->references('id')->on('customers')->cascadeOnDelete();
$table->index(['customer_id', 'status'], 'gc_credit_cust_st_idx');
});
Schema::create('gold_credit_installments', function (Blueprint $table) {
$table->id();
$table->unsignedBigInteger('gold_credit_id');
$table->unsignedSmallInteger('installment_number');
$table->date('due_date');
$table->unsignedInteger('amount_soot');
$table->decimal('principal_gold', 12, 6)->default(0);
$table->decimal('interest_gold', 12, 6)->default(0);
$table->decimal('total_gold', 12, 6)->default(0);
$table->decimal('paid_gold', 12, 6)->default(0);
$table->unsignedBigInteger('paid_rial')->default(0);
$table->enum('status', ['pending', 'paid', 'partial', 'overdue', 'prepaid'])->default('pending');
$table->timestamp('paid_at')->nullable();
$table->timestamps();
$table->foreign('gold_credit_id', 'gc_inst_credit_fk')
->references('id')->on('gold_credits')->cascadeOnDelete();
$table->unique(['gold_credit_id', 'installment_number'], 'gc_inst_num_uniq');
});
Schema::create('gold_credit_requests', function (Blueprint $table) {
$table->id();
$table->unsignedBigInteger('customer_id');
$table->decimal('requested_gold', 12, 4);
$table->unsignedSmallInteger('requested_months')->nullable();
$table->text('note')->nullable();
$table->enum('status', ['pending', 'approved', 'rejected'])->default('pending');
$table->text('admin_note')->nullable();
$table->unsignedBigInteger('gold_credit_id')->nullable();
$table->timestamps();
$table->foreign('customer_id', 'gc_req_customer_fk')
->references('id')->on('customers')->cascadeOnDelete();
$table->foreign('gold_credit_id', 'gc_req_credit_fk')
->references('id')->on('gold_credits')->nullOnDelete();
$table->index(['customer_id', 'status'], 'gc_req_cust_st_idx');
});
}
public function down(): void
{
Schema::dropIfExists('gold_credit_requests');
Schema::dropIfExists('gold_credit_installments');
Schema::dropIfExists('gold_credits');
}
};
@@ -0,0 +1,56 @@
<?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
{
Schema::table('gold_credits', function (Blueprint $table) {
$table->boolean('has_check')->default(false)->after('notes');
$table->boolean('has_contract')->default(false)->after('has_check');
$table->string('check_note')->nullable()->after('has_contract');
$table->string('contract_note')->nullable()->after('check_note');
$table->enum('penalty_type', ['rial', 'gold'])->nullable()->after('contract_note');
$table->decimal('penalty_amount', 14, 4)->nullable()->after('penalty_type');
$table->unsignedSmallInteger('grace_hours')->nullable()->after('penalty_amount');
});
Schema::table('gold_credit_installments', function (Blueprint $table) {
$table->timestamp('warning_sms_sent_at')->nullable()->after('paid_at');
$table->timestamp('penalty_started_at')->nullable()->after('warning_sms_sent_at');
$table->timestamp('last_penalty_at')->nullable()->after('penalty_started_at');
$table->unsignedInteger('penalty_days')->default(0)->after('last_penalty_at');
$table->unsignedBigInteger('penalty_rial')->default(0)->after('penalty_days');
$table->decimal('penalty_gold', 12, 6)->default(0)->after('penalty_rial');
});
}
public function down(): void
{
Schema::table('gold_credits', function (Blueprint $table) {
$table->dropColumn([
'has_check',
'has_contract',
'check_note',
'contract_note',
'penalty_type',
'penalty_amount',
'grace_hours',
]);
});
Schema::table('gold_credit_installments', function (Blueprint $table) {
$table->dropColumn([
'warning_sms_sent_at',
'penalty_started_at',
'last_penalty_at',
'penalty_days',
'penalty_rial',
'penalty_gold',
]);
});
}
};
@@ -0,0 +1,23 @@
<?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
{
Schema::table('gold_credit_installments', function (Blueprint $table) {
$table->timestamp('reminder_3d_sent_at')->nullable()->after('warning_sms_sent_at');
$table->timestamp('reminder_1d_sent_at')->nullable()->after('reminder_3d_sent_at');
});
}
public function down(): void
{
Schema::table('gold_credit_installments', function (Blueprint $table) {
$table->dropColumn(['reminder_3d_sent_at', 'reminder_1d_sent_at']);
});
}
};
@@ -0,0 +1,73 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
if (! Schema::hasTable('settings')) {
return;
}
$rows = [
// SuperAdmin-owned feature flag (default off). Local copy; overlay may overwrite.
[
'key' => 'enable_gold_credit',
'value' => 'false',
'title_fa' => 'اعتبار طلایی',
'description' => 'روشن: منوی اعتبار در ادمین و صفحه اعتبار در اپ مشتری فعال می‌شود. خاموش: غیرفعال. فقط از سوپرادمین قابل تغییر است.',
'can_edit' => 0,
'is_active' => 1,
'type' => 'switch',
],
['key' => 'gold_credit_penalty_type', 'value' => 'rial', 'title_fa' => 'نوع جریمه اعتبار طلایی', 'description' => 'ریالی یا طلایی|rial,gold', 'can_edit' => 1, 'is_active' => 1, 'type' => 'select'],
['key' => 'gold_credit_penalty_amount', 'value' => '1000000', 'title_fa' => 'مبلغ جریمه روزانه اعتبار طلایی', 'description' => '', 'can_edit' => 1, 'is_active' => 1, 'type' => 'number'],
['key' => 'gold_credit_grace_hours', 'value' => '24', 'title_fa' => 'مهلت پرداخت بعد از سررسید (ساعت)', 'description' => '', 'can_edit' => 1, 'is_active' => 1, 'type' => 'number'],
['key' => 'gold_credit_default_months', 'value' => '12', 'title_fa' => 'تعداد ماه پیش‌فرض اعتبار طلایی', 'description' => '', 'can_edit' => 1, 'is_active' => 1, 'type' => 'number'],
['key' => 'gold_credit_min_balance_gram', 'value' => '0.5', 'title_fa' => 'حداقل موجودی طلا برای درخواست (گرم)', 'description' => '', 'can_edit' => 1, 'is_active' => 1, 'type' => 'number'],
['key' => 'gold_credit_overdue_sms_enabled', 'value' => 'true', 'title_fa' => 'ارسال SMS هشدار سررسید اعتبار طلایی', 'description' => '', 'can_edit' => 1, 'is_active' => 1, 'type' => 'switch'],
['key' => 'gold_credit_admin_sms_enabled', 'value' => 'true', 'title_fa' => 'پیامک ادمین برای درخواست و پرداخت اعتبار طلایی', 'description' => '', 'can_edit' => 1, 'is_active' => 1, 'type' => 'switch'],
['key' => 'gold_credit_upcoming_sms_enabled', 'value' => 'true', 'title_fa' => 'یادآوری ۱ و ۳ روز مانده به سررسید قسط', 'description' => '', 'can_edit' => 1, 'is_active' => 1, 'type' => 'switch'],
['key' => 'gold_credit_pay_gold_enabled', 'value' => 'true', 'title_fa' => 'پرداخت اقساط با طلا', 'description' => '', 'can_edit' => 1, 'is_active' => 1, 'type' => 'switch'],
['key' => 'gold_credit_pay_rial_enabled', 'value' => 'false', 'title_fa' => 'پرداخت اقساط با ریال', 'description' => '', 'can_edit' => 1, 'is_active' => 1, 'type' => 'switch'],
['key' => 'gold_credit_pay_mixed_enabled', 'value' => 'false', 'title_fa' => 'پرداخت اقساط ترکیبی', 'description' => '', 'can_edit' => 1, 'is_active' => 1, 'type' => 'switch'],
['key' => 'gold_credit_overdue_sms_text', 'value' => 'مشتری گرامی، قسط اعتبار طلایی شما سررسید شده است.', 'title_fa' => 'متن SMS سررسید اعتبار طلایی', 'description' => '', 'can_edit' => 1, 'is_active' => 1, 'type' => 'longtext'],
['key' => 'gold_credit_info_text', 'value' => '', 'title_fa' => 'متن راهنمای اعتبار طلایی', 'description' => '', 'can_edit' => 1, 'is_active' => 1, 'type' => 'longtext'],
['key' => 'gold_credit_request_success_text', 'value' => 'درخواست شما ثبت شد و حداکثر تا ۲۴ ساعت بررسی می‌شود.', 'title_fa' => 'متن پس از ثبت درخواست اعتبار', 'description' => '', 'can_edit' => 1, 'is_active' => 1, 'type' => 'longtext'],
['key' => 'gold_credit_tiers', 'value' => '[]', 'title_fa' => 'سطوح اعتبار طلایی (JSON)', 'description' => '', 'can_edit' => 1, 'is_active' => 1, 'type' => 'longtext'],
];
$hasType = Schema::hasColumn('settings', 'type');
$hasShow = Schema::hasColumn('settings', 'show_to_user');
foreach ($rows as $row) {
$exists = DB::table('settings')->where('key', $row['key'])->exists();
if ($exists) {
continue;
}
$insert = [
'key' => $row['key'],
'value' => $row['value'],
'title_fa' => $row['title_fa'],
'description' => $row['description'],
'can_edit' => $row['can_edit'],
'is_active' => $row['is_active'],
];
if ($hasType) {
$insert['type'] = $row['type'];
}
if ($hasShow && $row['key'] === 'enable_gold_credit') {
$insert['show_to_user'] = 1;
}
DB::table('settings')->insert($insert);
}
}
public function down(): void
{
// keep data
}
};