Files
boursecu_server/app/Http/Controllers/Pargar/PargarInboundController.php
T
2026-07-20 15:52:42 +03:30

654 lines
24 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\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();
$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();
$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();
$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);
}
$this->sync()->confirmOrders($orderIds);
return response()->json([
'success' => true,
'message' => 'تاییدیه اسناد با موفقیت دریافت شد',
'data' => null,
]);
}
public function storeDocuments(Request $request)
{
$data = $this->pargarPayload($request);
$accountId = trim((string) ($data['accountId'] ?? ''));
$transactions = $data['transactions'] ?? [];
$customer = $accountId !== ''
? Customer::where('pargar_account_id', $accountId)->first()
: null;
PargarSyncLog::create([
'customer_id' => $customer?->id,
'desc' => 'دریافت اسناد از پرگار',
'type' => PargarSyncService::TYPE_DOCUMENT,
'sync_status' => PargarSyncService::STATUS_SUCCESS,
'pargar_ref' => $accountId,
'data' => json_encode($data, JSON_UNESCAPED_UNICODE),
]);
return response()->json([
'success' => true,
'message' => 'اسناد مشتری با موفقیت دریافت شد',
'data' => null,
]);
}
public function storeMande(Request $request)
{
$data = $this->pargarPayload($request);
$accountId = trim((string) ($data['AccountId'] ?? ''));
$customer = Customer::where('pargar_account_id', $accountId)->first();
if (! $customer) {
return response()->json([
'success' => false,
'message' => 'مشتری یافت نشد',
'data' => null,
], 422);
}
$goldInv = isset($data['GoldInv']) ? (float) str_replace('/', '.', (string) $data['GoldInv']) : 0;
$rialInv = isset($data['RialInv']) ? (float) str_replace('/', '.', (string) $data['RialInv']) : 0;
// فقط مقایسه برای لاگ — اپ source of truth است؛ کیف را از مانده پرگار عوض نکن
$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;
$desc = $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,
]), JSON_UNESCAPED_UNICODE),
]);
return response()->json([
'success' => true,
'message' => 'مانده مشتری با موفقیت دریافت شد',
'data' => null,
]);
}
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);
}
}
}
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 = (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 = 1;
$weight = $amount;
if ($isCountBased && $number > 0) {
$count = max(1, (int) round($number));
$weight = $amount;
} elseif ($number > 0 && abs($number - $amount) > 0.0001) {
$count = max(1, (int) round($number));
}
$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);
$tradesMainGold = $tradedProductId <= 0
|| app(ConfigService::class)->isMainWalletProduct($tradedProductId);
// اثر مانده از روش پرداخت؛ وزن طلا فقط وقتی کالای معامله یا پرداخت به کیف اصلی مربوط است
if ($paymentKind === PargarSyncService::PAYMENT_CASH) {
if ($tradesMainGold) {
// خرید/فروش آبشده کیف‌اصلی با نقدی: وزن + مبلغ
$itemId = $sync->pargarItemIdGold();
} else {
// محصول غیر اصلی با پرداخت نقدی: فقط ریال
$rialItemId = $sync->pargarItemIdRial();
$itemId = $rialItemId > 0 ? $rialItemId : $sync->pargarItemIdGold();
$weight = 0;
$count = 1;
}
} elseif ($paymentKind === PargarSyncService::PAYMENT_MAIN_GOLD) {
$itemId = $sync->pargarItemIdGold();
$itemName = $sync->mainWalletGoldItemName();
$paymentGold = (float) ($payload['payment_gold'] ?? $amount);
$weight = abs($paymentGold);
$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' => 1,
'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' => 1,
'Weight' => 0,
'Price' => abs($signed),
'Carat' => 750,
'Description' => $isDeposit ? 'واریز ریالی' : 'برداشت ریالی',
];
}
}