Files
boursecu_server/app/Http/Controllers/Pargar/PargarInboundController.php
T

727 lines
28 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
namespace App\Http\Controllers\Pargar;
use App\Http\Controllers\Controller;
use App\Http\Middleware\PargarAuth;
use App\Models\Cart;
use App\Models\Customer;
use App\Models\Order;
use App\Models\PargarSyncLog;
use App\Services\ConfigService;
use App\Services\PargarInboundWalletService;
use App\Services\PargarSyncService;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Schema;
use Morilog\Jalali\Jalalian;
use Rooyesh\Wallet\interfaces\WalletInterface;
use Symfony\Component\HttpKernel\Exception\UnprocessableEntityHttpException;
class PargarInboundController extends Controller
{
protected function sync(): PargarSyncService
{
return app(PargarSyncService::class);
}
public function login(Request $request)
{
if (! app(ConfigService::class)->isPargarAccounting()) {
return response()->json([
'success' => false,
'data' => null,
'message' => 'Pargar integration is disabled',
], 503);
}
[$username, $password] = $this->pargarLoginCredentials($request);
$expectedUser = $this->normalizeCredential(trim((string) $this->Config('pargar_username')));
$expectedPass = $this->normalizeCredential(trim((string) $this->Config('pargar_password')));
if ($username !== $expectedUser || $password !== $expectedPass || $expectedUser === '') {
return response()->json([
'success' => false,
'data' => null,
'message' => 'Invalid credentials!',
]);
}
$token = PargarAuth::issueToken($username);
return response()->json([
'success' => true,
'data' => ['token' => $token],
'message' => 'You are logged in successfully.',
]);
}
public function upsertCustomers(Request $request)
{
$payload = $this->pargarPayload($request);
$customers = $payload['Customers'] ?? [];
if (! is_array($customers) || $customers === []) {
return response()->json([
'success' => false,
'message' => 'Customers list is required',
'data' => null,
], 422);
}
$messages = [];
foreach ($customers as $row) {
if (! is_array($row)) {
continue;
}
$accountId = trim((string) ($row['AccountId'] ?? ''));
$fullName = trim((string) ($row['FullName'] ?? ''));
$mobile = Customer::normalizeMobile((string) ($row['CellPhone'] ?? ''));
if ($accountId === '' || $mobile === null) {
$messages[] = 'ردیف نامعتبر: AccountId یا CellPhone خالی است';
continue;
}
$byAccount = Customer::whereNull('deleted_at')
->where('pargar_account_id', $accountId)
->first();
if ($byAccount && Customer::normalizeMobile($byAccount->mobile) !== $mobile) {
$messages[] = "کد حساب {$accountId} قبلاً برای مشتری دیگری ثبت شده است";
continue;
}
$customer = Customer::findActiveByMobile($mobile);
if ($customer) {
$currentAccount = (string) ($customer->pargar_account_id ?? '');
if ($currentAccount === $accountId) {
$messages[] = 'کد حساب مشتری وجود دارد';
continue;
}
if ($currentAccount !== '' && $currentAccount !== $accountId) {
$messages[] = "شماره {$mobile} قبلاً به حساب پرگار دیگری متصل است";
continue;
}
$customer->pargar_account_id = $accountId;
$this->applyPargarCustomerName($customer, $fullName);
$customer->save();
app(PargarInboundWalletService::class)->queueWalletSyncAfterResponse((int) $customer->id);
$messages[] = "کد حسابداری مشتری {$fullName} ویرایش شد";
continue;
}
if ($byAccount) {
if (Customer::isMobileTaken($mobile, (int) $byAccount->id)) {
$messages[] = "شماره {$mobile} تکراری است";
continue;
}
if ($fullName !== '') {
$this->applyPargarCustomerName($byAccount, $fullName);
}
$byAccount->mobile = $mobile;
if (empty($byAccount->password)) {
$byAccount->password = $mobile;
}
$byAccount->save();
app(PargarInboundWalletService::class)->queueWalletSyncAfterResponse((int) $byAccount->id);
$messages[] = "مشتری با کد حسابداری {$accountId} به‌روزرسانی شد";
continue;
}
try {
$customer = new Customer();
$this->applyPargarCustomerName($customer, $fullName !== '' ? $fullName : 'مشتری');
$customer->mobile = $mobile;
$customer->pargar_account_id = $accountId;
$customer->active = true;
$customer->type = 2;
$customer->password = $mobile;
$customer->referral_id = $this->uniqueReferralId();
$customer->save();
app(PargarInboundWalletService::class)->queueWalletSyncAfterResponse((int) $customer->id);
$messages[] = "مشتری با کد حسابداری {$accountId} ایجاد شد";
} catch (\Throwable $e) {
$messages[] = "خطا برای {$mobile}: " . (
$e instanceof UnprocessableEntityHttpException
? $e->getMessage()
: 'ثبت مشتری ناموفق بود'
);
}
}
return response()->json([
'success' => true,
'message' => implode(' | ', $messages),
'data' => null,
]);
}
public function listOrders(Request $request)
{
$documents = collect();
$orders = Order::query()
->where('sts', 2)
->whereNull('pargar_confirmed_at')
->limit(100)
->get();
foreach ($orders as $order) {
$payload = $this->orderExportPayload($order);
if ($payload === null) {
continue;
}
$documents->push($payload);
}
$walletLogs = PargarSyncLog::query()
->whereIn('type', [PargarSyncService::TYPE_WALLET_CASH, PargarSyncService::TYPE_WALLET_GOLD])
->where('sync_status', PargarSyncService::STATUS_PENDING)
->orderBy('id')
->limit(100)
->get();
foreach ($walletLogs as $log) {
$payload = $this->walletExportPayload($log);
if ($payload !== null) {
$documents->push($payload);
}
}
return response()->json([
'state' => 'true',
'msg' => '',
'data' => [
'documents' => $documents->values(),
'duplicated' => 'false',
'result' => 'OK',
],
]);
}
public function confirmOrders(Request $request)
{
$payload = $this->pargarPayload($request);
$orderIds = $payload['OrderIds'] ?? [];
if (! is_array($orderIds)) {
return response()->json([
'success' => false,
'message' => 'OrderIds must be an array',
'data' => null,
], 422);
}
$result = $this->sync()->confirmOrders($orderIds);
// سند در پرگار ثبت شد → ماندهٔ معطل‌مانده را همگام کن
foreach ($result['customer_ids'] ?? [] as $customerId) {
$this->inboundWallet()->queueWalletSyncAfterResponse((int) $customerId);
}
return response()->json([
'success' => true,
'message' => 'تاییدیه اسناد با موفقیت دریافت شد',
'data' => null,
]);
}
protected function inboundWallet(): PargarInboundWalletService
{
return app(PargarInboundWalletService::class);
}
public function storeDocuments(Request $request)
{
$data = $this->pargarPayload($request);
$accountId = trim((string) ($data['accountId'] ?? ''));
$summary = $this->sync()->summarizeInboundDocument($data);
$customer = $accountId !== ''
? Customer::where('pargar_account_id', $accountId)->first()
: null;
// فقط لاگ — replay تاریخچه روی کیف خاموش است؛ بعد از ذخیره، مانده به‌صورت خودکار هم‌تراز می‌شود
PargarSyncLog::create([
'customer_id' => $customer?->id,
'amount' => $summary['amount'],
'desc' => $customer
? ($summary['desc'] !== '' ? $summary['desc'] : 'دریافت اسناد از پرگار')
: 'دریافت اسناد از پرگار — مشتری یافت نشد',
'type' => PargarSyncService::TYPE_DOCUMENT,
'sync_status' => PargarSyncService::STATUS_SUCCESS,
'pargar_ref' => $accountId !== '' ? $accountId : null,
'data' => json_encode(array_merge($data, [
'_summary' => $summary,
'wallet_applied' => false,
'code_build' => 'docs-then-mande-auto',
]), JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT),
]);
\Illuminate\Support\Facades\Log::info('Pargar documents stored; queue auto mande sync', [
'account_id' => $accountId,
'customer_id' => $customer?->id,
'tx_count' => is_array($data['transactions'] ?? null) ? count($data['transactions']) : 0,
]);
// به‌محض ورود اسناد → ack + اعمال آخرین مانده
if ($customer) {
$this->inboundWallet()->queueWalletSyncAfterResponse((int) $customer->id);
}
return response()->json([
'success' => true,
'message' => 'اسناد ثبت شد؛ همگام‌سازی مانده در صف است',
'data' => [
'wallet_applied' => false,
'auto_mande_queued' => (bool) $customer,
'build' => 'docs-then-mande-auto',
],
]);
}
public function storeMande(Request $request)
{
$data = $this->pargarPayload($request);
$accountId = trim((string) ($data['AccountId'] ?? ''));
$customer = Customer::where('pargar_account_id', $accountId)->first();
$goldInv = isset($data['GoldInv']) ? (float) str_replace('/', '.', (string) $data['GoldInv']) : 0;
$rialInv = isset($data['RialInv']) ? (float) str_replace('/', '.', (string) $data['RialInv']) : 0;
if (! $customer) {
PargarSyncLog::create([
'customer_id' => null,
'desc' => 'مانده پرگار — مشتری هنوز در پنل نیست',
'type' => PargarSyncService::TYPE_MANDE,
'amount' => $goldInv,
'sync_status' => PargarSyncService::STATUS_SUCCESS,
'pargar_ref' => $accountId,
'data' => json_encode(array_merge($data, [
'orphan_mande' => true,
'wallet_adjusted' => false,
]), JSON_UNESCAPED_UNICODE),
]);
return response()->json([
'success' => true,
'message' => 'مانده ذخیره شد؛ پس از ثبت مشتری اعمال می‌شود',
'data' => null,
]);
}
$ownerType = (int) ($customer->type ?? 2);
$walletInterface = new WalletInterface($customer->id, $ownerType);
$wallet = $walletInterface->wallet;
$appGold = (float) ($wallet->gold ?? 0);
$appCash = (float) ($wallet->cash ?? 0);
$goldDiff = $goldInv - $appGold;
$rialDiff = $rialInv - $appCash;
$hasDiff = abs($goldDiff) >= 0.01 || abs($rialDiff) >= 1;
$pendingOutbound = $this->inboundWallet()->hasPendingOutboundToPargar($customer);
$desc = $pendingOutbound
? 'مانده دریافت شد — منتظر ثبت سند پنل در پرگار'
: ($hasDiff
? 'اختلاف مانده پرگار با اپ — در صف همگام‌سازی خودکار'
: 'دریافت مانده از پرگار — بدون اختلاف');
PargarSyncLog::create([
'customer_id' => $customer->id,
'desc' => $desc,
'type' => PargarSyncService::TYPE_MANDE,
'amount' => $goldInv,
'sync_status' => PargarSyncService::STATUS_SUCCESS,
'pargar_ref' => $accountId,
'data' => json_encode(array_merge($data, [
'app_gold' => $appGold,
'app_cash' => $appCash,
'gold_diff' => $goldDiff,
'rial_diff' => $rialDiff,
'wallet_adjusted' => false,
'awaiting_outbound_confirm' => $pendingOutbound,
]), JSON_UNESCAPED_UNICODE),
]);
// همیشه صف همگام‌سازی: اگر سند pending نباشد اعمال می‌شود؛ وگرنه بعد از تأیید سند اعمال می‌شود
$this->inboundWallet()->queueWalletSyncAfterResponse((int) $customer->id);
return response()->json([
'success' => true,
'message' => $pendingOutbound
? 'مانده ذخیره شد؛ بعد از ثبت سند در پرگار خودکار سینک می‌شود'
: 'مانده دریافت شد؛ همگام‌سازی خودکار در صف است',
'data' => [
'awaiting_outbound_confirm' => $pendingOutbound,
'auto_sync_queued' => true,
],
]);
}
public function listNewCustomers(Request $request)
{
$customers = Customer::query()
->whereNull('deleted_at')
->where(function ($q) {
$q->whereNull('pargar_account_id')
->orWhere('pargar_account_id', '')
->orWhere('pargar_account_id', '0')
->orWhere('pargar_account_id', '-1');
})
->limit(100)
->get(['id', 'fname', 'lname', 'name', 'mobile']);
if ($customers->isEmpty()) {
return response()->json([
'success' => true,
'message' => 'مشتری جدید وجود ندارد',
'data' => null,
]);
}
$rows = $customers->map(function (Customer $customer) {
$fname = trim((string) ($customer->fname ?? ''));
$lname = trim((string) ($customer->lname ?? ''));
if ($fname === '' && $lname === '') {
$nameParts = preg_split('/\s+/', trim((string) ($customer->name ?? '')), 2) ?: ['', ''];
$fname = trim((string) ($nameParts[0] ?? ''));
$lname = trim((string) ($nameParts[1] ?? ''));
}
return [
'id' => (string) $customer->id,
'name' => $fname,
'family' => $lname,
'mobile' => (string) $customer->mobile,
];
});
return response()->json([
'state' => 'true',
'msg' => '',
'customer' => $rows,
]);
}
public function confirmCustomers(Request $request)
{
$payload = $this->pargarPayload($request);
$rows = $payload['customers'] ?? [];
if (! is_array($rows)) {
return response()->json([
'success' => false,
'message' => 'customers must be an array',
'data' => null,
], 422);
}
foreach ($rows as $row) {
if (! is_array($row)) {
continue;
}
$customerId = $row['customer_id'] ?? $row['id_customer'] ?? null;
$accountId = $row['accounting_id'] ?? null;
if ($customerId === null || $accountId === null) {
continue;
}
$normalizedAccountId = trim((string) $accountId);
if ($normalizedAccountId === '' || in_array($normalizedAccountId, ['0', '-1'], true)) {
continue;
}
$customer = Customer::find($customerId);
if ($customer) {
$customer->pargar_account_id = $normalizedAccountId;
$customer->save();
$log = PargarSyncLog::query()
->where('customer_id', $customer->id)
->where('type', 'customer')
->where('sync_status', PargarSyncService::STATUS_PENDING)
->latest('id')
->first();
if ($log) {
$this->sync()->markSuccess($log, $normalizedAccountId);
}
app(PargarInboundWalletService::class)->queueWalletSyncAfterResponse((int) $customer->id);
}
}
return response()->json([
'success' => true,
'message' => 'تاییدیه ثبت مشتری با موفقیت دریافت شد',
'data' => null,
]);
}
/**
* @return array{0: string, 1: string}
*/
private function pargarLoginCredentials(Request $request): array
{
$payload = $this->pargarPayload($request);
return [
$this->normalizeCredential((string) ($payload['username'] ?? '')),
$this->normalizeCredential((string) ($payload['password'] ?? '')),
];
}
/**
* @return array<string, mixed>
*/
private function pargarPayload(Request $request): array
{
$payload = $request->all();
if ($payload !== []) {
return $payload;
}
$decoded = json_decode((string) $request->getContent(), true);
return is_array($decoded) ? $decoded : [];
}
private function normalizeCredential(string $value): string
{
$persian = ['۰', '۱', '۲', '۳', '۴', '۵', '۶', '۷', '۸', '۹'];
$arabic = ['٠', '١', '٢', '٣', '٤', '٥', '٦', '٧', '٨', '٩'];
$latin = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'];
return trim(str_replace(array_merge($persian, $arabic), array_merge($latin, $latin), $value));
}
private function applyPargarCustomerName(Customer $customer, string $fullName): void
{
$fullName = trim($fullName);
if ($fullName === '') {
return;
}
[$fname, $lname] = $this->splitPargarFullName($fullName);
$customer->fname = $fname;
$customer->lname = $lname;
// mutator مدل Customer مقدار name را از fname + lname می‌سازد
$customer->name = trim("$fname $lname");
}
/**
* @return array{0: string, 1: string}
*/
private function splitPargarFullName(string $fullName): array
{
$parts = preg_split('/\s+/u', trim($fullName), 2) ?: ['', ''];
return [
trim((string) ($parts[0] ?? '')),
trim((string) ($parts[1] ?? '')),
];
}
private function uniqueReferralId(): string
{
do {
$referral = substr(str_shuffle('0123456789abcdefghijklmnopqrstuvwxyz'), 0, 4);
} while (Customer::where('referral_id', $referral)->exists());
return $referral;
}
private function pargarItemName(Order $order): string
{
return $this->isCartOrder($order) ? 'سبد خرید' : 'آبشده';
}
private function isCartOrder(Order $order): bool
{
if (Schema::hasTable('cart') && Schema::hasColumn('cart', 'order_id')) {
return Cart::where('order_id', $order->id)->exists();
}
return PargarSyncLog::where('order_id', $order->id)
->whereIn('desc', ['سبد خرید', 'فروش سبد به مشتری'])
->exists();
}
/**
* @return array<string, mixed>|null
*/
private function orderExportPayload(Order $order): ?array
{
$sync = $this->sync();
$log = PargarSyncLog::where('order_id', $order->id)->latest('id')->first();
$payload = $log ? (json_decode((string) $log->data, true) ?: []) : [];
$paymentKind = (string) ($payload['payment_kind'] ?? PargarSyncService::PAYMENT_CASH);
if ($paymentKind === PargarSyncService::PAYMENT_OTHER_PRODUCT) {
return null;
}
if ($log && is_string($log->pargar_ref) && str_starts_with($log->pargar_ref, 'skip:')) {
return null;
}
$customer = Customer::find((int) ($order->customer_id ?? 0));
$isCustomerBuy = (int) ($order->direction ?? 1) === 1;
$customerDirection = (int) ($order->direction ?? 0);
$fromCart = (bool) ($payload['from_cart'] ?? $this->isCartOrder($order));
$product = $sync->resolveOrderProduct($order, $payload);
$number = (float) ($payload['number'] ?? 0);
$amount = abs((float) ($order->amount ?? 0));
$totalPrice = (float) ($order->total_price ?? $order->payment_amount ?? 0);
$unitPrice = (float) ($order->price ?? 0);
$isCountBased = $product
? self::isCountBasedProduct(self::productUnitPayload($product))
: false;
// پرگار: Count>0 → سند تعدادی (وزن را نادیده می‌گیرد)
// آبشده/وزنی مثل آذین: Count=0 و Weight=گرم
$count = 0;
$weight = $amount;
if ($isCountBased) {
$count = $number > 0 ? max(1, (int) round($number)) : 1;
$weight = $amount;
}
$itemId = $sync->pargarItemIdGold();
$itemName = $this->pargarItemName($order);
if ($product && trim((string) $product->title) !== '') {
$itemName = trim((string) $product->title);
}
$tradedProductId = (int) ($payload['product_id'] ?? $order->product_id ?? 0);
// فقط محصولات main_wallet_products روی GoldInv آبشده اثر دارند.
// group_type=آبشده به‌تنهایی کافی نیست (مثلاً آبشده سرمایه‌گذاری / قلک).
if ($tradedProductId > 0) {
$tradesMainGold = app(ConfigService::class)->isMainWalletProduct($tradedProductId);
} else {
$tradesMainGold = $product && self::isMeltedTradeGroup($product->group_type ?? null);
}
// اثر مانده از روش پرداخت؛ وزن طلا فقط وقتی کالای معامله به کیف اصلی مربوط است
if ($paymentKind === PargarSyncService::PAYMENT_CASH) {
if ($tradesMainGold) {
// خرید/فروش آبشده کیف‌اصلی با نقدی: وزن + مبلغ — حتماً تعدادی نباشد
$itemId = $sync->pargarItemIdGold();
$count = 0;
$weight = $amount;
} else {
// محصول غیر اصلی با پرداخت نقدی: فقط ریال (بدون وزن طلای پرگار)
$rialItemId = $sync->pargarItemIdRial();
$itemId = $rialItemId > 0 ? $rialItemId : $sync->pargarItemIdGold();
$weight = 0;
$count = 0;
}
} elseif ($paymentKind === PargarSyncService::PAYMENT_MAIN_GOLD) {
$itemId = $sync->pargarItemIdGold();
$itemName = $sync->mainWalletGoldItemName();
$paymentGold = abs((float) ($payload['payment_gold'] ?? $amount));
$weight = $paymentGold;
$count = 0;
$totalPrice = 0;
}
return [
'OrderId' => (string) $order->id,
'OrderType' => $isCustomerBuy ? -1 : 1,
'PriceType' => 1,
'CustomerAccountId' => (int) ($customer->pargar_account_id ?? 0),
'ItemId' => $itemId,
'ItemName' => $itemName,
'OrderDate' => $order->pay_at
? Jalalian::fromDateTime($order->pay_at)->format('Y/m/d-H:i:s')
: Jalalian::fromDateTime($order->created_at)->format('Y/m/d-H:i:s'),
'Fee' => $unitPrice,
'Count' => $count,
'Weight' => $weight,
'Price' => $totalPrice,
'Carat' => 750,
'Description' => self::accountingShopTradeDesc($customerDirection, $fromCart),
];
}
/**
* @return array<string, mixed>|null
*/
private function walletExportPayload(PargarSyncLog $log): ?array
{
$sync = $this->sync();
$payload = json_decode((string) $log->data, true) ?: [];
$kind = (string) ($payload['kind'] ?? ($log->type === PargarSyncService::TYPE_WALLET_GOLD ? 'gold' : 'cash'));
$signed = (float) ($payload['signed_amount'] ?? $log->amount ?? 0);
if (abs($signed) < ($kind === 'cash' ? 1 : 0.01)) {
$signed = (float) ($log->amount ?? 0);
// اگر علامت نبود، از desc حدس بزن — پیش‌فرض واریز مثبت
if (! isset($payload['signed_amount']) && is_string($log->desc) && str_contains($log->desc, 'برداشت')) {
$signed = -abs($signed);
} elseif (! isset($payload['signed_amount'])) {
$signed = abs($signed);
}
}
$customer = Customer::find((int) ($log->customer_id ?? 0));
$withdrawId = (int) ($log->withdraw_id ?? 0);
$orderId = $withdrawId > 0 ? 'W-' . $withdrawId : 'WLOG-' . $log->id;
$isDeposit = $signed >= 0;
// OrderType از دید فروشگاه: واریز به مشتری = فروشگاه بدهکار؟ قرارداد قبلی خرید مشتری = -1
// واریز ریالی مشتری ≈ افزایش بدهی فروشگاه به مشتری از نظر حساب → مشابه خرید نیست؛
// برای واریز: OrderType=1 (ورود وجه به حساب مشتری در پرگار بسته به نرم‌افزار)
// هم‌راستا با آذین: اسناد کیف را با OrderType بر اساس علامت می‌فرستیم
$orderType = $isDeposit ? 1 : -1;
if ($kind === 'gold') {
return [
'OrderId' => $orderId,
'OrderType' => $orderType,
'PriceType' => 1,
'CustomerAccountId' => (int) ($customer->pargar_account_id ?? 0),
'ItemId' => $sync->pargarItemIdGold(),
'ItemName' => $sync->mainWalletGoldItemName(),
'OrderDate' => Jalalian::fromDateTime($log->date ?? $log->created_at)->format('Y/m/d-H:i:s'),
'Fee' => 0,
'Count' => 0,
'Weight' => abs($signed),
'Price' => 0,
'Carat' => 750,
'Description' => $isDeposit ? 'شارژ طلای کیف اصلی' : 'برداشت طلای کیف اصلی',
];
}
$rialItemId = $sync->pargarItemIdRial();
return [
'OrderId' => $orderId,
'OrderType' => $orderType,
'PriceType' => 1,
'CustomerAccountId' => (int) ($customer->pargar_account_id ?? 0),
'ItemId' => $rialItemId > 0 ? $rialItemId : $sync->pargarItemIdGold(),
'ItemName' => $isDeposit ? 'واریز ریالی' : 'برداشت ریالی',
'OrderDate' => Jalalian::fromDateTime($log->date ?? $log->created_at)->format('Y/m/d-H:i:s'),
'Fee' => 0,
'Count' => 0,
'Weight' => 0,
'Price' => abs($signed),
'Carat' => 750,
'Description' => $isDeposit ? 'واریز ریالی' : 'برداشت ریالی',
];
}
}