fix zarin duplicate hesab and convert mesghal unit price to gram before cash/weight.

Reuse an existing Zarin hesab instead of a second addhesab, and stop rial-locked orders from treating mesghal as per-gram.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-08-29 13:50:12 +03:30
co-authored by Cursor
parent 6b5b490e2b
commit c1e2bb5e9b
3 changed files with 171 additions and 17 deletions
+102 -17
View File
@@ -11,6 +11,7 @@ use App\Models\Zarin;
use App\Services\AccountingProductDocMapper;
use App\Services\ZarinSyncService;
use Carbon\Carbon;
use Illuminate\Contracts\Cache\LockTimeoutException;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Http;
@@ -1146,6 +1147,79 @@ class ZarinController extends Controller
return '';
}
/** کد حساب زرین از پاسخ addhesab / listhesabha — OK و Code هم مثل idHesab معتبرند */
private function extractZarinHesabId(array $row): ?int
{
foreach (['idHesab', 'idhesab', 'Code', 'code', 'OK', 'id'] as $key) {
if (! array_key_exists($key, $row)) {
continue;
}
$value = $row[$key];
if ($value === null || $value === '' || $value === false || $value === true) {
continue;
}
if (is_numeric($value) && (int) $value > 0) {
return (int) $value;
}
}
return null;
}
/** حساب از قبل ساخته‌شده در زرین — با موبایل، تا addhesab دوم نسازیم */
private function findExistingZarinHesabId(?string $mobile): ?int
{
$want = Customer::normalizeMobile($mobile ?? '');
if ($want === null) {
return null;
}
$fetched = $this->fetchAllZarinCustomers();
$rows = $fetched['rows'] ?? [];
if ($rows === []) {
return null;
}
$groupFilter = $this->zarinCustomerGroupName();
$sameGroup = null;
$any = null;
foreach ($rows as $item) {
if (! is_array($item)) {
continue;
}
$phone = $this->extractZarinPhone($item);
$got = Customer::normalizeMobile($phone);
if ($got === null || $got !== $want) {
continue;
}
$id = $this->extractZarinHesabId($item);
if ($id === null) {
continue;
}
$g = $this->extractZarinGroup($item);
if ($groupFilter !== '' && $g !== '' && $this->zarinGroupNamesMatch($g, $groupFilter)) {
$sameGroup = $id;
break;
}
$any ??= $id;
}
return $sameGroup ?? $any;
}
private function bindCustomerZarinHesab(Customer $customer, int $idHesab): int
{
$customer->moshtari_code = $idHesab;
$customer->save();
return $idHesab;
}
public function syncPanelToZarin(): \Illuminate\Http\JsonResponse
{
$customers = Customer::whereNull('deleted_at')
@@ -1450,7 +1524,7 @@ class ZarinController extends Controller
$isColleagueLeg = ! empty($data['colleague_leg']) && $accountOverride !== '';
if (! $isColleagueLeg) {
if ($customer && ($customer->moshtari_code === null)) {
if ($customer && ! $customer->hasValidMoshtariCode()) {
$this->createCustomerInZarin($customer->id);
$customer = Customer::where('id', $data['customer_id'])->first();
}
@@ -1472,6 +1546,9 @@ class ZarinController extends Controller
$count = $mapper->accountingQty($product, $rawAmount, $data);
$vazn = $mapper->accountingWeight($product, $rawAmount, $data);
$isBuy = (int) ($data['direction'] ?? 0) === 1;
// زرین korf/parDar از دید حساب مشتری است، نه فروشگاه:
// خرید مشتری → k / +1 : طلا بستانکار (سوت اضافه) + ریال بدهکار (پول کم)
// فروش مشتری → f / −1 : طلا بدهکار + ریال بستانکار
$orderId = is_object($data['order'] ?? null)
? ($data['order']->id ?? null)
: ($data['order']['id'] ?? ($existing->order_id ?? null));
@@ -1530,7 +1607,7 @@ class ZarinController extends Controller
$accountCustomer,
$product,
$count,
$isBuy ? -1 : 1,
$isBuy ? 1 : -1,
$fi,
$sharh,
$this->zarinUid($uidKind, $uidSource ?? $zarin->id)
@@ -1541,7 +1618,7 @@ class ZarinController extends Controller
$data,
$accountCustomer,
$vazn,
$isBuy ? 'f' : 'k',
$isBuy ? 'k' : 'f',
(float) ($data['price'] ?? 0),
$sharh,
$mapper->jensFelez($product) ?? 0,
@@ -1576,7 +1653,7 @@ class ZarinController extends Controller
$customer = Customer::where('id', $data['customer_id'])->first();
if ($customer && ($customer->moshtari_code === null)) {
if ($customer && ! $customer->hasValidMoshtariCode()) {
$this->createCustomerInZarin($customer->id);
$customer = Customer::where('id', $data['customer_id'])->first();
}
@@ -1698,7 +1775,7 @@ class ZarinController extends Controller
return false;
}
if ($customer->moshtari_code === null) {
if (! $customer->hasValidMoshtariCode()) {
$this->createCustomerInZarin($customer->id);
$customer = Customer::where('id', $data['customer_id'])->first();
}
@@ -1806,7 +1883,7 @@ class ZarinController extends Controller
$customer = Customer::where('id', $data['customer_id'])->first();
if ($customer && ($customer->moshtari_code === null)) {
if ($customer && ! $customer->hasValidMoshtariCode()) {
$this->createCustomerInZarin($customer->id);
$customer = Customer::where('id', $data['customer_id'])->first();
}
@@ -2066,16 +2143,18 @@ class ZarinController extends Controller
{
$customer = Customer::findOrFail($customer_id);
if ($customer->moshtari_code != null) {
if ($customer->hasValidMoshtariCode()) {
return $customer->moshtari_code;
}
$lock = Cache::lock('zarin:register-customer:' . $customer->id, 30);
$lock = Cache::lock('zarin:register-customer:' . $customer->id, 90);
if (!$lock->get()) {
try {
$lock->block(25);
} catch (LockTimeoutException $e) {
$customer->refresh();
if ($customer->moshtari_code != null) {
if ($customer->hasValidMoshtariCode()) {
return $customer->moshtari_code;
}
@@ -2085,10 +2164,15 @@ class ZarinController extends Controller
try {
$customer->refresh();
if ($customer->moshtari_code != null) {
if ($customer->hasValidMoshtariCode()) {
return $customer->moshtari_code;
}
$existingId = $this->findExistingZarinHesabId($customer->mobile);
if ($existingId !== null) {
return $this->bindCustomerZarinHesab($customer, $existingId);
}
$name = $customer->fname ?: ($customer->name ?? ('مشتری ' . $customer->id));
$family = $customer->lname ?: '';
$mobile = $customer->mobile ?? '';
@@ -2119,14 +2203,15 @@ class ZarinController extends Controller
}
$data = $this->extractResponseRow($response);
$idHesab = $data['idHesab'] ?? null;
$idHesab = $this->extractZarinHesabId($data);
if (!empty($idHesab)) {
$customer->moshtari_code = $idHesab;
$customer->save();
if ($idHesab !== null) {
return $this->bindCustomerZarinHesab($customer, $idHesab);
}
// int برگردان تا با پیام خطای string اشتباه نشود
return is_numeric($idHesab) ? (0 + $idHesab) : $idHesab;
$existingId = $this->findExistingZarinHesabId($customer->mobile);
if ($existingId !== null) {
return $this->bindCustomerZarinHesab($customer, $existingId);
}
$detail = $data['msg'] ?? $data['message'] ?? $data['detail'] ?? null;
@@ -99,6 +99,7 @@ class SingleOrderController extends Controller
} else if (auth('user')->id()) {//admin
$data['service_charge_percentage'] = 0;
}
$this->ensureOrderUnitPriceIsPerGram($data);
if($data['price'] === 0){
throw new UnprocessableEntityHttpException(' سفارشات بسته میباشد.');
}
@@ -320,6 +321,7 @@ class SingleOrderController extends Controller
$feeRate = isset($data['fee_free']) && $data['fee_free'] === true
? 0.0
: (float) ($data['service_charge_percentage'] ?? 0);
$this->ensureOrderUnitPriceIsPerGram($data);
$data['price'] += $data['difference'];
if ($rialLocked) {
@@ -586,6 +588,7 @@ class SingleOrderController extends Controller
$feeRate = isset($data['fee_free']) && $data['fee_free'] === true
? 0.0
: (float) ($data['service_charge_percentage'] ?? 0);
$this->ensureOrderUnitPriceIsPerGram($data);
$data['price'] -= $data['difference'];
if ($rialLocked) {
@@ -1330,6 +1333,44 @@ class SingleOrderController extends Controller
return round($amount, 10);
}
/** مظنهٔ زندهٔ هر گرم (ریال) — برای تشخیص قیمت مثقال به‌جای گرم */
private function liveGramQuoteRial(): float
{
$base = (float) app(ProductDisplayPriceService::class)->resolveAppBasePrice();
if ($base > 0) {
return $base;
}
$montzar = (float) app(ConfigService::class)->get('montzar_live_price', 0);
if ($montzar > 0) {
return $montzar;
}
return (float) (Price::latest()->first()?->price ?? 0);
}
/**
* اگر قیمت واحد سفارش حدود قیمت مثقال باشد، به قیمت هر گرم برگردان.
* جلوگیری از ۱۰ میلیون تومان → ۱۰۵ سوت وقتی مثقال در فیلد «هر گرم» نشسته.
*/
private function ensureOrderUnitPriceIsPerGram(array &$data): void
{
$live = $this->liveGramQuoteRial();
$config = app(ConfigService::class);
$price = $config->gramUnitPriceIfMesghalQuoted((float) ($data['price'] ?? 0), $live);
if ($price <= 0 || abs($price - (float) ($data['price'] ?? 0)) < 1) {
return;
}
$data['price'] = $price;
if (isset($data['price_original'])) {
$data['price_original'] = $config->gramUnitPriceIfMesghalQuoted(
(float) $data['price_original'],
$live
);
}
}
private function resolveGoldPaymentProductId(array $data): ?int
{
if (!empty($data['payment_product_id'])) {