From 25ee47b47341d070ccb97d8e0b99c1960e2150f6 Mon Sep 17 00:00:00 2001 From: safavimohadeseh78 Date: Tue, 20 Oct 2026 10:11:03 -0400 Subject: [PATCH] digipay --- .../abshode-domain-lookup/learned-notes.md | 1 + .../Admin/DigipayInstallmentController.php | 44 +++ .../Customer/DigipayInstallmentController.php | 53 ++++ app/Services/AdminPageAccess.php | 2 + app/Services/DigipayInstallmentService.php | 253 ++++++++++++++++++ .../2026_10_20_000002_digipay_installment.php | 130 +++++++++ routes/admin.php | 4 + routes/customer.php | 5 + 8 files changed, 492 insertions(+) create mode 100644 app/Http/Controllers/Admin/DigipayInstallmentController.php create mode 100644 app/Http/Controllers/Customer/DigipayInstallmentController.php create mode 100644 app/Services/DigipayInstallmentService.php create mode 100644 database/migrations/2026_10_20_000002_digipay_installment.php diff --git a/.cursor/skills/abshode-domain-lookup/learned-notes.md b/.cursor/skills/abshode-domain-lookup/learned-notes.md index ed5a768..27ff1f5 100644 --- a/.cursor/skills/abshode-domain-lookup/learned-notes.md +++ b/.cursor/skills/abshode-domain-lookup/learned-notes.md @@ -18,6 +18,7 @@ - 2026-09-16 | تعداد کد هدیه | `GiftCardController::redeem` | count=N → یک رکورد giftcards با count=N + N ردیف giftcard_codes؛ مثل storeRedeem - 2026-09-12 | انقضای کارت هدیه | `GiftCardController::validateGiftExpiryDate` + `GiftCardEditorFormBody` | تاریخ قبل از امروز → ۴۰۰ / خطای فرم؛ امروز مجاز - 2026-09-09 | رمز ادمین ≠ لاگین اپ | `Customer::setPasswordAttribute` + `AuthController` + `updateAdmin` | قبل از هش/لاگین ارقام فارسی→انگلیسی؛ jwt hash=false؛ آنلاین فقط password غیرخالی بفرست +- 2026-10-20 | خرید اقساطی دیجی‌پی | `DigipayInstallmentService` + جداول `digipay_credits`/`digipay_orders` + migration `2026_10_20_000002` | فلگ پیش‌فرض خاموش؛ اعتبار را ادمین ست می‌کند نه وب‌هوک دیجی‌پی - 2026-10-20 | دسترسی صفحات مدیر | `users.page_access` + `AdminPageAccess` + ادمین `adminPageAccess.ts` / ویرایش مدیران | null=کامل؛ درخت مشاهده/ویرایش/حذف؛ ورود=اولین صفحه مجاز؛ منوی ممنوع=«شما به این صفحه دسترسی ندارید» - 2026-10-20 | ویرایش/حذف کارت هدیه | `GiftCardController::update/destroy` + `giftCards/components/table.vue` | PUT/DELETE `/admin/giftcard/{id}`؛ کارت استفاده‌شده حذف نمی‌شود و مقدار/تعدادش عوض نمی‌شود - 2026-10-20 | دکمه ویرایش/حذف گیفت | `allowsPageFlag` + `giftCards/components/table.vue` | بدون edit/delete روی giftCardMain دکمه و دیالوگ باز نمی‌شود diff --git a/app/Http/Controllers/Admin/DigipayInstallmentController.php b/app/Http/Controllers/Admin/DigipayInstallmentController.php new file mode 100644 index 0000000..d5eb091 --- /dev/null +++ b/app/Http/Controllers/Admin/DigipayInstallmentController.php @@ -0,0 +1,44 @@ +ensureEnabled(); + $data = $request->validate([ + 'mobile' => 'required|string|max:20', + 'plan4_toman' => 'required|integer|min:0', + 'plan12_toman' => 'required|integer|min:0', + ]); + + return response()->json($this->service->setCredit( + $data['mobile'], + (int) $data['plan4_toman'], + (int) $data['plan12_toman'] + )); + } + + public function orders() + { + $this->ensureEnabled(); + + return response()->json(['data' => $this->service->recentOrders()]); + } + + private function ensureEnabled(): void + { + if (! $this->service->enabled()) { + abort(403, 'خرید اقساطی فعال نیست'); + } + } +} diff --git a/app/Http/Controllers/Customer/DigipayInstallmentController.php b/app/Http/Controllers/Customer/DigipayInstallmentController.php new file mode 100644 index 0000000..72b6d3b --- /dev/null +++ b/app/Http/Controllers/Customer/DigipayInstallmentController.php @@ -0,0 +1,53 @@ +ensureEnabled(); + + return response()->json($this->service->statusFor((int) auth('customer')->id())); + } + + public function start() + { + $this->ensureEnabled(); + $this->service->markStarted((int) auth('customer')->id()); + + return response()->json($this->service->statusFor((int) auth('customer')->id())); + } + + public function buy(Request $request) + { + $this->ensureEnabled(); + $data = $request->validate([ + 'plan' => 'required|integer|in:4,12', + 'amount_toman' => 'required|integer|min:1000', + ]); + + $result = $this->service->buy( + (int) auth('customer')->id(), + (int) $data['plan'], + (int) $data['amount_toman'] + ); + + return response()->json($result); + } + + private function ensureEnabled(): void + { + if (! $this->service->enabled()) { + abort(403, 'خرید اقساطی فعال نیست'); + } + } +} diff --git a/app/Services/AdminPageAccess.php b/app/Services/AdminPageAccess.php index 76c9cb0..54ad880 100644 --- a/app/Services/AdminPageAccess.php +++ b/app/Services/AdminPageAccess.php @@ -16,6 +16,7 @@ class AdminPageAccess 'customerMain_online', 'customerMain_specialPrice', 'customerMain_goldCredit', + 'customerMain_digipayInstallment', 'productsMain_limitedOrder', 'productsMain_archive', 'productsMain_categories', @@ -62,6 +63,7 @@ class AdminPageAccess 'customerMain_archive' => ['restore', 'info'], 'customerMain_specialPrice' => ['edit', 'toggle', 'delete'], 'customerMain_goldCredit' => ['detail', 'edit', 'approve', 'reject'], + 'customerMain_digipayInstallment' => ['edit'], 'productsMain_limitedOrder' => ['buy', 'sell', 'list', 'tv', 'edit', 'archive'], 'productsMain_archive' => ['restore', 'info'], 'ordersMain' => ['info'], diff --git a/app/Services/DigipayInstallmentService.php b/app/Services/DigipayInstallmentService.php new file mode 100644 index 0000000..568b58b --- /dev/null +++ b/app/Services/DigipayInstallmentService.php @@ -0,0 +1,253 @@ +getBool('enable_digipay_installment', false); + } + + public function publicConfig(): array + { + $cfg = app(ConfigService::class); + + return [ + 'onboarding_url' => trim($cfg->getString('digipay_onboarding_url', '')), + 'fee_shop_percent' => (float) $cfg->getString('digipay_fee_shop_percent', '8'), + 'fee_gateway_percent' => (float) $cfg->getString('digipay_fee_gateway_percent', '5.7'), + 'fee_extra_percent' => (float) $cfg->getString('digipay_fee_extra_percent', '2.3'), + 'plan4_cap_toman' => (int) $cfg->getString('digipay_plan4_cap_toman', '30000000'), + 'plan12_cap_toman' => (int) $cfg->getString('digipay_plan12_cap_toman', '200000000'), + ]; + } + + public function statusFor(int $customerId): array + { + $credit = DB::table('digipay_credits')->where('customer_id', $customerId)->first(); + $config = $this->publicConfig(); + + return [ + 'enabled' => true, + 'status' => $credit->status ?? 'none', + 'plan4_remaining_toman' => (int) floor(((int) ($credit->plan4_remaining_rial ?? 0)) / 10), + 'plan12_remaining_toman' => (int) floor(((int) ($credit->plan12_remaining_rial ?? 0)) / 10), + 'gram_price_rial' => $this->gramPriceRial(), + 'config' => $config, + ]; + } + + public function markStarted(int $customerId): void + { + $config = $this->publicConfig(); + if ($config['onboarding_url'] === '') { + throw new UnprocessableEntityHttpException('لینک دیجی‌پی در تنظیمات ثبت نشده است'); + } + + $existing = DB::table('digipay_credits')->where('customer_id', $customerId)->first(); + if ($existing && $existing->status === 'active') { + return; + } + + if ($existing) { + DB::table('digipay_credits')->where('id', $existing->id)->update([ + 'status' => 'pending', + 'updated_at' => now(), + ]); + return; + } + + DB::table('digipay_credits')->insert([ + 'customer_id' => $customerId, + 'status' => 'pending', + 'plan4_remaining_rial' => 0, + 'plan12_remaining_rial' => 0, + 'created_at' => now(), + 'updated_at' => now(), + ]); + } + + public function setCredit(string $mobile, int $plan4Toman, int $plan12Toman): array + { + $customer = Customer::findActiveByMobile($mobile); + if (! $customer) { + throw new UnprocessableEntityHttpException('مشتری با این موبایل پیدا نشد'); + } + + $row = [ + 'status' => 'active', + 'plan4_remaining_rial' => max(0, $plan4Toman) * 10, + 'plan12_remaining_rial' => max(0, $plan12Toman) * 10, + 'updated_at' => now(), + ]; + + $existing = DB::table('digipay_credits')->where('customer_id', $customer->id)->first(); + if ($existing) { + DB::table('digipay_credits')->where('id', $existing->id)->update($row); + } else { + $row['customer_id'] = $customer->id; + $row['created_at'] = now(); + DB::table('digipay_credits')->insert($row); + } + + return [ + 'customer_id' => (int) $customer->id, + 'name' => $customer->name, + 'plan4_remaining_toman' => max(0, $plan4Toman), + 'plan12_remaining_toman' => max(0, $plan12Toman), + ]; + } + + public function buy(int $customerId, int $plan, int $amountToman): array + { + if (! in_array($plan, [4, 12], true)) { + throw new UnprocessableEntityHttpException('طرح اقساط نامعتبر است'); + } + if ($amountToman < 1000) { + throw new UnprocessableEntityHttpException('مبلغ خرید را وارد کنید'); + } + + $price = $this->gramPriceRial(); + if ($price <= 0) { + throw new UnprocessableEntityHttpException('قیمت لحظه‌ای در دسترس نیست'); + } + + $config = $this->publicConfig(); + $cap = $plan === 4 ? $config['plan4_cap_toman'] : $config['plan12_cap_toman']; + if ($amountToman > $cap) { + throw new UnprocessableEntityHttpException('مبلغ از سقف این طرح بیشتر است'); + } + + $feePercent = $config['fee_shop_percent'] + $config['fee_gateway_percent'] + $config['fee_extra_percent']; + $amountRial = $amountToman * 10; + $chargeRial = (int) round($amountRial * (1 + ($feePercent / 100))); + $feeRial = $chargeRial - $amountRial; + $grams = round($amountRial / $price, 4); + if ($grams <= 0) { + throw new UnprocessableEntityHttpException('مقدار مس محاسبه‌شده نامعتبر است'); + } + + $column = $plan === 4 ? 'plan4_remaining_rial' : 'plan12_remaining_rial'; + + return DB::transaction(function () use ($customerId, $plan, $amountRial, $feeRial, $grams, $price, $chargeRial, $column) { + $credit = DB::table('digipay_credits')->where('customer_id', $customerId)->lockForUpdate()->first(); + if (! $credit || $credit->status !== 'active') { + throw new UnprocessableEntityHttpException('اعتبار اقساطی شما هنوز فعال نشده است'); + } + if ((int) $credit->{$column} < $chargeRial) { + throw new UnprocessableEntityHttpException('مانده اعتبار این طرح کافی نیست'); + } + + DB::table('digipay_credits')->where('id', $credit->id)->update([ + $column => (int) $credit->{$column} - $chargeRial, + 'updated_at' => now(), + ]); + + $orderId = DB::table('digipay_orders')->insertGetId([ + 'customer_id' => $customerId, + 'plan' => $plan, + 'amount_rial' => $amountRial, + 'fee_rial' => $feeRial, + 'grams' => $grams, + 'gram_price_rial' => $price, + 'status' => 'done', + 'created_at' => now(), + 'updated_at' => now(), + ]); + + $this->creditMetal($customerId, $grams, (int) $orderId); + + return [ + 'order_id' => (int) $orderId, + 'grams' => $grams, + 'plan' => $plan, + 'charge_toman' => (int) floor($chargeRial / 10), + ]; + }); + } + + public function recentOrders(int $limit = 50): array + { + return DB::table('digipay_orders as o') + ->leftJoin('customers as c', 'c.id', '=', 'o.customer_id') + ->orderByDesc('o.id') + ->limit($limit) + ->get([ + 'o.id', + 'o.customer_id', + 'c.name', + 'c.mobile', + 'o.plan', + 'o.amount_rial', + 'o.fee_rial', + 'o.grams', + 'o.created_at', + ]) + ->map(function ($row) { + return [ + 'id' => (int) $row->id, + 'customer_id' => (int) $row->customer_id, + 'name' => $row->name, + 'mobile' => $row->mobile, + 'plan' => (int) $row->plan, + 'amount_toman' => (int) floor(((int) $row->amount_rial) / 10), + 'fee_toman' => (int) floor(((int) $row->fee_rial) / 10), + 'grams' => (float) $row->grams, + 'created_at' => $row->created_at, + ]; + }) + ->all(); + } + + private function gramPriceRial(): int + { + return (int) (Price::query()->orderByDesc('id')->value('price') ?? 0); + } + + private function creditMetal(int $customerId, float $grams, int $orderId): void + { + $user = Customer::query()->find($customerId); + if (! $user) { + throw new UnprocessableEntityHttpException('مشتری پیدا نشد'); + } + + $withdraw = Withdraw::create([ + 'customer_id' => $user->id, + 'gold' => $grams, + 'note' => 'خرید اقساطی', + 'sts' => 6, + 'tracked_number' => $orderId, + ]); + + $wallet = new WalletInterface($user->id, $user->type); + new TransactionInterface($wallet, [ + 'cash' => 0, + 'gold' => $grams, + 'expire_at' => now(), + 'note' => 'خرید اقساطی', + 'withdraw_id' => $withdraw->id, + 'active' => true, + ], 7, true, false); + + try { + app(ShayeganSyncService::class)->recordWalletGold([ + 'customer_id' => $user->id, + 'value' => $grams, + 'direction' => 1, + 'withdraw_id' => $withdraw->id, + 'note' => 'خرید اقساطی', + ]); + } catch (\Throwable $e) { + report($e); + } + } +} diff --git a/database/migrations/2026_10_20_000002_digipay_installment.php b/database/migrations/2026_10_20_000002_digipay_installment.php new file mode 100644 index 0000000..a01423b --- /dev/null +++ b/database/migrations/2026_10_20_000002_digipay_installment.php @@ -0,0 +1,130 @@ +id(); + $table->unsignedBigInteger('customer_id')->unique(); + $table->string('status', 20)->default('pending'); + $table->unsignedBigInteger('plan4_remaining_rial')->default(0); + $table->unsignedBigInteger('plan12_remaining_rial')->default(0); + $table->timestamps(); + }); + } + + if (! Schema::hasTable('digipay_orders')) { + Schema::create('digipay_orders', function (Blueprint $table) { + $table->id(); + $table->unsignedBigInteger('customer_id')->index(); + $table->unsignedTinyInteger('plan'); + $table->unsignedBigInteger('amount_rial'); + $table->unsignedBigInteger('fee_rial'); + $table->decimal('grams', 16, 4); + $table->unsignedBigInteger('gram_price_rial'); + $table->string('status', 20)->default('done'); + $table->timestamps(); + }); + } + + if (! Schema::hasTable('settings')) { + return; + } + + $rows = [ + [ + 'key' => 'enable_digipay_installment', + 'value' => 'false', + 'title_fa' => 'خرید اقساطی مس', + 'description' => 'روشن: منوی خرید اقساطی در اپ و صفحه اعتبار دیجی‌پی در ادمین.', + 'type' => 'switch', + 'show' => 1, + ], + [ + 'key' => 'digipay_onboarding_url', + 'value' => '', + 'title_fa' => 'لینک احراز دیجی‌پی', + 'description' => 'لینکی که دیجی‌پی بعد از قرارداد می‌دهد. کاربر برای احراز هویت به همین آدرس می‌رود.', + 'type' => 'text', + 'show' => 0, + ], + [ + 'key' => 'digipay_fee_shop_percent', + 'value' => '8', + 'title_fa' => 'کارمزد خرید اقساطی (درصد)', + 'description' => '', + 'type' => 'number', + 'show' => 1, + ], + [ + 'key' => 'digipay_fee_gateway_percent', + 'value' => '5.7', + 'title_fa' => 'کارمزد دیجی‌پی (درصد)', + 'description' => '', + 'type' => 'number', + 'show' => 1, + ], + [ + 'key' => 'digipay_fee_extra_percent', + 'value' => '2.3', + 'title_fa' => 'کارمزد جانبی اقساط (درصد)', + 'description' => '', + 'type' => 'number', + 'show' => 1, + ], + [ + 'key' => 'digipay_plan4_cap_toman', + 'value' => '30000000', + 'title_fa' => 'سقف اعتبار ۴ قسط (تومان)', + 'description' => '', + 'type' => 'number', + 'show' => 1, + ], + [ + 'key' => 'digipay_plan12_cap_toman', + 'value' => '200000000', + 'title_fa' => 'سقف وام ۱۲ قسط (تومان)', + 'description' => '', + 'type' => 'number', + 'show' => 1, + ], + ]; + + $hasType = Schema::hasColumn('settings', 'type'); + $hasShow = Schema::hasColumn('settings', 'show_to_user'); + + foreach ($rows as $row) { + if (DB::table('settings')->where('key', $row['key'])->exists()) { + continue; + } + $insert = [ + 'key' => $row['key'], + 'value' => $row['value'], + 'title_fa' => $row['title_fa'], + 'description' => $row['description'], + 'can_edit' => 1, + 'is_active' => 1, + ]; + if ($hasType) { + $insert['type'] = $row['type']; + } + if ($hasShow) { + $insert['show_to_user'] = $row['show']; + } + DB::table('settings')->insert($insert); + } + } + + public function down(): void + { + Schema::dropIfExists('digipay_orders'); + Schema::dropIfExists('digipay_credits'); + } +}; diff --git a/routes/admin.php b/routes/admin.php index 64e319c..774d1e6 100644 --- a/routes/admin.php +++ b/routes/admin.php @@ -23,6 +23,7 @@ use App\Http\Controllers\Admin\ShayeganController; use App\Http\Controllers\Admin\FileController; use App\Http\Controllers\Site\OfferCodesController; use App\Http\Controllers\Admin\SettingsController; +use App\Http\Controllers\Admin\DigipayInstallmentController as AdminDigipayInstallmentController; use App\Http\Controllers\Admin\GoldCreditController as AdminGoldCreditController; use \App\Http\Controllers\Admin\GiftCardController; use App\Http\Controllers\Admin\CategoryController; @@ -184,6 +185,9 @@ Route::middleware(['auth:user', 'admin.page'])->group(function (){ Route::put('/gold_credit/{id}', [AdminGoldCreditController::class, 'update']); Route::post('/gold_credit', [AdminGoldCreditController::class, 'store']); + Route::post('/digipay_installment/credit', [AdminDigipayInstallmentController::class, 'setCredit']); + Route::post('/digipay_installment/orders', [AdminDigipayInstallmentController::class, 'orders']); + Route::post('/wallet/charge_wallet',[WalletController::class,'charge_wallet']); Route::get('/wallet/charge-targets',[WalletController::class,'chargeTargets']); Route::post('/wallet/table',[WalletController::class,'indexWallet']); diff --git a/routes/customer.php b/routes/customer.php index dea36b6..273a3f0 100644 --- a/routes/customer.php +++ b/routes/customer.php @@ -1,6 +1,7 @@ group(function () use ( Route::post('gold_credit/request', [GoldCreditController::class, 'requestLoan']); Route::post('gold_credit/installments/pay', [GoldCreditController::class, 'payInstallments']); + Route::get('digipay_installment', [DigipayInstallmentController::class, 'show']); + Route::post('digipay_installment/start', [DigipayInstallmentController::class, 'start']); + Route::post('digipay_installment/buy', [DigipayInstallmentController::class, 'buy']); + if ($isStore) { Route::post('vc_validate', [havaleController::class, 'virtualCardValidate']); Route::post('havale', [havaleController::class, 'havale']);