fix some bugs from accounting
This commit is contained in:
@@ -10,6 +10,7 @@ use Illuminate\Support\Facades\Http;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use App\Models\Tahesab;
|
||||
use App\Models\Customer;
|
||||
use App\Services\AccountingProductDocMapper;
|
||||
use App\Services\TahesabSyncService;
|
||||
use Morilog\Jalali\Jalalian;
|
||||
use Symfony\Component\HttpKernel\Exception\UnprocessableEntityHttpException;
|
||||
@@ -421,6 +422,10 @@ class TahesabController extends Controller
|
||||
if (empty($data['customer_id']) && $existing && $existing->order_id) {
|
||||
$order = \App\Models\Order::find($existing->order_id);
|
||||
if ($order) {
|
||||
$productPayload = ['metal_id' => $existing->type === 'طلا' ? 1 : 2];
|
||||
if ($order->product_id) {
|
||||
$productPayload['id'] = (int) $order->product_id;
|
||||
}
|
||||
$data = array_merge($data, [
|
||||
'customer_id' => $existing->customer_id,
|
||||
'amount' => $existing->amount,
|
||||
@@ -428,8 +433,9 @@ class TahesabController extends Controller
|
||||
'price' => $order->price,
|
||||
'total_price' => $order->total_price,
|
||||
'pay_at' => $existing->date,
|
||||
'product' => ['metal_id' => $existing->type === 'طلا' ? 1 : 2],
|
||||
'order' => ['id' => $existing->order_id],
|
||||
'product' => $productPayload,
|
||||
'product_id' => $order->product_id ? (int) $order->product_id : null,
|
||||
'order' => ['id' => $existing->order_id, 'product_id' => $order->product_id],
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -440,71 +446,91 @@ class TahesabController extends Controller
|
||||
: false;
|
||||
}
|
||||
|
||||
// $customer = auth()->user();
|
||||
// if (!$customer) {
|
||||
// throw new UnprocessableEntityHttpException("Auth needed");
|
||||
// }
|
||||
$mapper = app(AccountingProductDocMapper::class);
|
||||
$product = $mapper->resolveProductFromPayload($data);
|
||||
if ($product && $mapper->shouldSkip($product)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$customer = Customer::where('id', $data['customer_id'])->first();
|
||||
|
||||
if ($customer && ($customer->moshtari_code === null)) {
|
||||
$this->createCustomerInTahesab($customer->id);
|
||||
$customer = Customer::where('id', $data['customer_id'])->first();
|
||||
}
|
||||
$date = Verta::instance(Carbon::parse($data['pay_at']))->format('Y-m-d');
|
||||
|
||||
$kind = $mapper->resolveKind($product);
|
||||
$rawAmount = (float) $data['amount'];
|
||||
$count = $mapper->accountingQty($product, $rawAmount, $data);
|
||||
$vazn = $mapper->accountingWeight($product, $rawAmount, $data);
|
||||
$jens = $mapper->jensFelez($product) ?? 0;
|
||||
$title = $mapper->productTitle($product);
|
||||
$isBuy = (int) ($data['direction'] ?? 0) === 1;
|
||||
$price = (float) ($data['price'] ?? 0);
|
||||
$mablaghKol = $isBuy
|
||||
? (int) round($vazn * $price)
|
||||
: (int) ($data['total_price'] ?? round($vazn * $price));
|
||||
// سند تعدادی: مظنه = فی هر عدد (نه لزوماً قیمت هر گرم)
|
||||
$mazaneh = (int) $price;
|
||||
if ($mapper->isCountDoc($product) && $count > 0) {
|
||||
if (!empty($data['total_price'])) {
|
||||
$mazaneh = (int) round(((float) $data['total_price']) / $count);
|
||||
} elseif ($price > 0 && abs($vazn - $count) > 0.0001) {
|
||||
// price احتمالاً هر گرم است
|
||||
$unitW = $vazn / $count;
|
||||
$mazaneh = (int) round($price * $unitW);
|
||||
}
|
||||
if ($mazaneh <= 0 && $mablaghKol > 0) {
|
||||
$mazaneh = (int) round($mablaghKol / $count);
|
||||
}
|
||||
}
|
||||
|
||||
$date = Verta::instance(Carbon::parse($data['pay_at'] ?? now()))->format('Y-m-d');
|
||||
[$year, $month, $day] = explode('-', $date);
|
||||
|
||||
$amount = $data['amount'];
|
||||
$isBuy = (int) ($data['direction'] ?? 0) === 1;
|
||||
$mablaghKol = $isBuy
|
||||
? (int) round((float) $amount * (float) $data['price'])
|
||||
: (int) ($data['total_price'] ?? round((float) $amount * (float) $data['price']));
|
||||
$action = $isBuy ? 'خرید' : 'فروش';
|
||||
$kindFa = $mapper->kindLabel($kind);
|
||||
$sharh = trim($title !== '' ? "{$action} {$title}" : "{$action} {$kindFa}");
|
||||
|
||||
$data['handler'] = 'storeTrade';
|
||||
if ($existing && empty($existing->data)) {
|
||||
$existing->update(['data' => json_encode($data, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT)]);
|
||||
}
|
||||
$tahesab = $existing ?? Tahesab::create([
|
||||
"customer_id" => $customer->id,
|
||||
"desc" => $data['direction'] == 1 ? "خرید آبشده" : "فروش آبشده",
|
||||
"amount" => $amount,
|
||||
"order_id" => $data['order']['id'] ?? null,
|
||||
"date" => $data['pay_at'],
|
||||
"type" => $data['product']['metal_id'] == 1 ? "طلا" : "نقره",
|
||||
"created_at" => Carbon::now(),
|
||||
"data" => json_encode($data, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT),
|
||||
'customer_id' => $customer->id,
|
||||
'desc' => $sharh,
|
||||
'amount' => $mapper->isCountDoc($product) ? $count : $vazn,
|
||||
'order_id' => $data['order']['id'] ?? null,
|
||||
'date' => $data['pay_at'] ?? Carbon::now(),
|
||||
'type' => $mapper->metalLabel($product),
|
||||
'created_at' => Carbon::now(),
|
||||
'data' => json_encode($data, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT),
|
||||
]);
|
||||
|
||||
if ($mapper->isCountDoc($product) && $title === '') {
|
||||
$this->sync()->markFailed($tahesab, null, 'نام محصول برای سند تعدادی تهحساب خالی است');
|
||||
return false;
|
||||
}
|
||||
|
||||
$body = $this->buildBuySaleBody(
|
||||
$kind,
|
||||
(int) $customer->moshtari_code,
|
||||
(int) $year,
|
||||
(int) $month,
|
||||
(int) $day,
|
||||
$vazn,
|
||||
$count,
|
||||
$title,
|
||||
$isBuy ? 1 : 0,
|
||||
$mazaneh,
|
||||
$mablaghKol,
|
||||
$sharh,
|
||||
$jens,
|
||||
(string) Verta::instance($data['pay_at'] ?? now())
|
||||
);
|
||||
|
||||
try {
|
||||
$response = Http::withoutVerifying()->timeout(10)->withHeaders([
|
||||
'DBName' => $this->Config('tahesab_db'),
|
||||
'Content-Type' => 'application/json',
|
||||
'Authorization' => "Bearer " . $this->Config('tahesab_token'),
|
||||
])->post($this->tahesabApiUrl(), [
|
||||
"DoNewSanadBuySaleGOLD" => [
|
||||
1,//Sabte_Kol_Or_Movaghat_1_0
|
||||
(int)$customer->moshtari_code, //Moshtari_Code
|
||||
-1, //Factor_Number
|
||||
1, //Radif_Number
|
||||
(int)$year, //Shamsi_Year
|
||||
(int)$month, //Shamsi_Month
|
||||
(int)$day, //Shamsi_Day
|
||||
(float)$amount, //Vazn
|
||||
$data['product']['metal_id'] == 1 ? 750 : "", //Ayar
|
||||
0,
|
||||
"",
|
||||
$data['direction'] == 1 ? 1 : 0, //BuyOrSale_0_1
|
||||
(int)$data['price'], //Mazaneh
|
||||
1, //MazanehIsMesghalOrGeram_0_1
|
||||
1, //IsMotefaregheOrAbshode_0_1
|
||||
$mablaghKol, //MablaghKol — خرید: وزن خالص × قیمت (بعد از کارمزد)
|
||||
$data['direction'] == 1 ? "خرید آبشده" : "فروش آبشده", //sharh
|
||||
-1, //Factor_Code — خودکار
|
||||
-1, //HavalehBe_Mcode
|
||||
-1, //MultiRadif
|
||||
$data['product']['metal_id'] == 1 ? 0 : 2, //Jens_Felez
|
||||
(string)Verta::instance($data['pay_at']),
|
||||
]
|
||||
])->json();
|
||||
$response = $this->postTahesab($body);
|
||||
} catch (\Exception $e) {
|
||||
$this->sync()->markFailed($tahesab, $e->getMessage());
|
||||
return $e->getMessage();
|
||||
@@ -517,7 +543,6 @@ class TahesabController extends Controller
|
||||
|
||||
$this->sync()->markFailed($tahesab, $response, 'ثبت سند معامله در تهحساب ناموفق بود');
|
||||
return false;
|
||||
|
||||
}
|
||||
|
||||
// directon: 0 = daryaft az moshtari
|
||||
@@ -802,7 +827,7 @@ class TahesabController extends Controller
|
||||
return false;
|
||||
}
|
||||
|
||||
// direction: 1 = واریز طلا (خرید)، 0 = برداشت طلا (فروش)
|
||||
// direction: 1 = واریز طلا (ورود)، 0 = برداشت طلا (خروج) — VKH نه BuySale
|
||||
public function walletGold($data, ?Tahesab $existing = null)
|
||||
{
|
||||
if (empty($data['customer_id']) && $existing) {
|
||||
@@ -812,6 +837,15 @@ class TahesabController extends Controller
|
||||
'direction' => str_contains((string) $existing->desc, 'برداشت') ? 0 : 1,
|
||||
'withdraw_id' => $existing->withdraw_id,
|
||||
]);
|
||||
$payload = json_decode((string) ($existing->data ?? ''), true);
|
||||
if (is_array($payload)) {
|
||||
if (!empty($payload['product_id'])) {
|
||||
$data['product_id'] = (int) $payload['product_id'];
|
||||
}
|
||||
if (!empty($payload['product']) && is_array($payload['product'])) {
|
||||
$data['product'] = $payload['product'];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!isset($data['customer_id'], $data['value'])) {
|
||||
@@ -820,6 +854,12 @@ class TahesabController extends Controller
|
||||
: false;
|
||||
}
|
||||
|
||||
$mapper = app(AccountingProductDocMapper::class);
|
||||
$product = $mapper->resolveProductFromPayload($data);
|
||||
if ($product && $mapper->shouldSkip($product)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$customer = Customer::where('id', $data['customer_id'])->first();
|
||||
|
||||
if ($customer && ($customer->moshtari_code === null)) {
|
||||
@@ -827,70 +867,65 @@ class TahesabController extends Controller
|
||||
$customer = Customer::where('id', $data['customer_id'])->first();
|
||||
}
|
||||
|
||||
$amount = abs($data['value']);
|
||||
$buyOrSale = (int) ($data['direction'] ?? 1) === 1 ? 1 : 0;
|
||||
$isDeposit = $buyOrSale === 1;
|
||||
$kind = $mapper->resolveKind($product);
|
||||
$rawValue = (float) $data['value'];
|
||||
$count = $mapper->accountingQty($product, $rawValue, $data);
|
||||
$vazn = $mapper->accountingWeight($product, $rawValue, $data);
|
||||
$jens = $mapper->jensFelez($product) ?? 0;
|
||||
$title = $mapper->productTitle($product);
|
||||
$isDeposit = (int) ($data['direction'] ?? 1) === 1;
|
||||
$isVorood = $isDeposit ? 1 : 0;
|
||||
|
||||
$action = $isDeposit ? 'شارژ' : 'دشارژ';
|
||||
$kindFa = $mapper->kindLabel($kind);
|
||||
$sharh = !empty($data['note'])
|
||||
? (string) $data['note']
|
||||
: (!empty($data['desc'])
|
||||
? (string) $data['desc']
|
||||
: trim($title !== '' ? "{$action} {$title}" : "{$action} {$kindFa}"));
|
||||
$data['note'] = $sharh;
|
||||
$data['handler'] = 'walletGold';
|
||||
|
||||
$tahesab = $existing ?? Tahesab::create([
|
||||
"customer_id" => $customer->id,
|
||||
"desc" => $isDeposit ? "شارژ کیف پول آبشده" : "برداشت از کیف پول آبشده",
|
||||
"amount" => $amount,
|
||||
"date" => Carbon::now(),
|
||||
"type" => "طلا",
|
||||
"created_at" => Carbon::now(),
|
||||
"withdraw_id" => $data['withdraw_id'] ?? null,
|
||||
"data" => json_encode($data, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT),
|
||||
'customer_id' => $customer->id,
|
||||
'desc' => $sharh,
|
||||
'amount' => $mapper->isCountDoc($product) ? $count : $vazn,
|
||||
'date' => Carbon::now(),
|
||||
'type' => $mapper->metalLabel($product),
|
||||
'created_at' => Carbon::now(),
|
||||
'withdraw_id' => $data['withdraw_id'] ?? null,
|
||||
'data' => json_encode($data, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT),
|
||||
]);
|
||||
|
||||
if ($existing && empty($existing->data)) {
|
||||
$existing->update(['data' => json_encode($data, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT)]);
|
||||
}
|
||||
|
||||
if ($mapper->isCountDoc($product) && $title === '') {
|
||||
$this->sync()->markFailed($tahesab, null, 'نام محصول برای سند تعدادی تهحساب خالی است');
|
||||
return false;
|
||||
}
|
||||
|
||||
$dateSource = ($existing !== null && !empty($existing->date)) ? Carbon::parse($existing->date) : Carbon::now();
|
||||
$date = Verta::instance($dateSource)->format('Y-m-d');
|
||||
[$year, $month, $day] = explode('-', $date);
|
||||
|
||||
$prices = $this->lastPriceData();
|
||||
|
||||
$mazaneh = $isDeposit ? (int) ($prices['buy'] ?? 0) : (int) ($prices['sell'] ?? 0);
|
||||
$mablaghKol = (int) round($mazaneh * $amount);
|
||||
$sharh = !empty($data['note'])
|
||||
? $data['note']
|
||||
: (!empty($data['desc']) ? $data['desc'] : ($isDeposit ? 'خرید آبشده' : 'فروش آبشده'));
|
||||
$data['note'] = $sharh;
|
||||
$body = $this->buildVkhBody(
|
||||
$kind,
|
||||
(int) $customer->moshtari_code,
|
||||
(int) $year,
|
||||
(int) $month,
|
||||
(int) $day,
|
||||
$vazn,
|
||||
$count,
|
||||
$title,
|
||||
$isVorood,
|
||||
$sharh,
|
||||
$jens
|
||||
);
|
||||
|
||||
try {
|
||||
$response = Http::withoutVerifying()->timeout(10)->withHeaders([
|
||||
'DBName' => $this->Config('tahesab_db'),
|
||||
'Content-Type' => 'application/json',
|
||||
'Authorization' => "Bearer " . $this->Config('tahesab_token'),
|
||||
])->post($this->tahesabApiUrl(), [
|
||||
"DoNewSanadBuySaleGOLD" => [
|
||||
1,//Sabte_Kol_Or_Movaghat_1_0
|
||||
(int)$customer->moshtari_code, //Moshtari_Code
|
||||
-1, //Factor_Number
|
||||
1, //Radif_Number
|
||||
(int)$year, //Shamsi_Year
|
||||
(int)$month, //Shamsi_Month
|
||||
(int)$day, //Shamsi_Day
|
||||
(float)$amount, //Vazn
|
||||
750, //Ayar
|
||||
0,
|
||||
"",
|
||||
$buyOrSale, //BuyOrSale_0_1 — 1=خرید، 0=فروش
|
||||
$mazaneh, //Mazaneh
|
||||
1, //MazanehIsMesghalOrGeram_0_1
|
||||
1, //IsMotefaregheOrAbshode_0_1
|
||||
$mablaghKol, //MablaghKol
|
||||
$sharh,//Sharh
|
||||
-1, //Factor_Code — خودکار
|
||||
-1, //HavalehBe_Mcode
|
||||
-1, //MultiRadif
|
||||
0, //Jens_Felez
|
||||
(string)$date,
|
||||
]
|
||||
])->json();
|
||||
$response = $this->postTahesab($body);
|
||||
} catch (\Exception $e) {
|
||||
$this->sync()->markFailed($tahesab, $e->getMessage());
|
||||
return $e->getMessage();
|
||||
@@ -903,7 +938,141 @@ class TahesabController extends Controller
|
||||
|
||||
$this->sync()->markFailed($tahesab, $response, 'ثبت سند طلایی کیف پول در تهحساب ناموفق بود');
|
||||
return false;
|
||||
}
|
||||
|
||||
private function postTahesab(array $body): array
|
||||
{
|
||||
$response = Http::withoutVerifying()->timeout(10)->withHeaders([
|
||||
'DBName' => $this->Config('tahesab_db'),
|
||||
'Content-Type' => 'application/json',
|
||||
'Authorization' => 'Bearer ' . $this->Config('tahesab_token'),
|
||||
])->post($this->tahesabApiUrl(), $body)->json();
|
||||
|
||||
return is_array($response) ? $response : [];
|
||||
}
|
||||
|
||||
/**
|
||||
* سند خرید/فروش — معامله.
|
||||
*/
|
||||
private function buildBuySaleBody(
|
||||
string $kind,
|
||||
int $moshtariCode,
|
||||
int $year,
|
||||
int $month,
|
||||
int $day,
|
||||
float $vazn,
|
||||
float $count,
|
||||
string $name,
|
||||
int $buyOrSale,
|
||||
int $mazaneh,
|
||||
int $mablaghKol,
|
||||
string $sharh,
|
||||
int $jens,
|
||||
string $zamanTasvie
|
||||
): array {
|
||||
$ayar = $jens === 0 ? 750 : '';
|
||||
|
||||
if ($kind === AccountingProductDocMapper::KIND_SEKEH) {
|
||||
return [
|
||||
'DoNewSanadBuySaleSEKEH' => [
|
||||
1, $moshtariCode, -1, 1, $year, $month, $day,
|
||||
(float) $vazn, $ayar, (float) $count, $name,
|
||||
$buyOrSale, $mazaneh, $mablaghKol, $sharh,
|
||||
-1, -1, -1, $jens, '',
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
if ($kind === AccountingProductDocMapper::KIND_SHEMSH) {
|
||||
return [
|
||||
'DoNewSanadBuySaleShemsh' => [
|
||||
1, $moshtariCode, -1, 1, $year, $month, $day,
|
||||
(float) $vazn, $ayar, (float) $count, $name,
|
||||
$buyOrSale, $mazaneh, $mablaghKol, $sharh,
|
||||
-1, -1, -1, $jens, '',
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
if ($kind === AccountingProductDocMapper::KIND_KAR) {
|
||||
return [
|
||||
'DoNewSanadBuySaleKar' => [
|
||||
1, $moshtariCode, -1, 1, $year, $month, $day,
|
||||
(float) $vazn, $ayar ?: 750, (float) $count, $name,
|
||||
$buyOrSale, $mazaneh, 1, $mablaghKol,
|
||||
0, 0, 0, 0, 0, 0, 0, '', 0, $sharh,
|
||||
-1, -1, -1, $jens, '',
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
// وزن / آبشده — همان payload قبلی با Jens_Felez و Sharh دقیقتر
|
||||
return [
|
||||
'DoNewSanadBuySaleGOLD' => [
|
||||
1, $moshtariCode, -1, 1, $year, $month, $day,
|
||||
(float) $vazn, $ayar, 0, '',
|
||||
$buyOrSale, $mazaneh, 1, 1, $mablaghKol, $sharh,
|
||||
-1, -1, -1, $jens, $zamanTasvie,
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* سند ورود/خروج — شارژ، دشارژ، برداشت فیزیکی.
|
||||
*/
|
||||
private function buildVkhBody(
|
||||
string $kind,
|
||||
int $moshtariCode,
|
||||
int $year,
|
||||
int $month,
|
||||
int $day,
|
||||
float $vazn,
|
||||
float $count,
|
||||
string $name,
|
||||
int $isVorood,
|
||||
string $sharh,
|
||||
int $jens
|
||||
): array {
|
||||
$ayar = $jens === 0 ? 750 : '';
|
||||
|
||||
if ($kind === AccountingProductDocMapper::KIND_SEKEH) {
|
||||
return [
|
||||
'DoNewSanadVKHSEKEH' => [
|
||||
1, $moshtariCode, -1, 1, $year, $month, $day,
|
||||
(float) $vazn, $ayar, (float) $count, $name,
|
||||
$isVorood, $sharh, -1, -1, -1, $jens,
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
if ($kind === AccountingProductDocMapper::KIND_SHEMSH) {
|
||||
return [
|
||||
'DoNewSanadVKHShemsh' => [
|
||||
1, $moshtariCode, -1, 1, $year, $month, $day,
|
||||
(float) $vazn, $ayar, (float) $count, $name,
|
||||
$isVorood, $sharh, -1, -1, -1, $jens,
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
if ($kind === AccountingProductDocMapper::KIND_KAR) {
|
||||
return [
|
||||
'DoNewSanadVKHKar' => [
|
||||
1, $moshtariCode, -1, 1, $year, $month, $day,
|
||||
(float) $vazn, $ayar ?: 750, (float) $count, $name,
|
||||
$isVorood, 0, 0, 0, 0, 0, 0, '', 0, 0, $sharh,
|
||||
-1, -1, -1, $jens,
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
'DoNewSanadVKHGOLD' => [
|
||||
1, $moshtariCode, -1, 1, $year, $month, $day,
|
||||
(float) $vazn, $ayar ?: 750, '', '',
|
||||
$isVorood, 1, $sharh, -1, -1, -1, $jens, 0,
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
public function createCustomerInTahesab($customer_id)
|
||||
|
||||
@@ -315,6 +315,18 @@ class WalletController extends Controller
|
||||
try {
|
||||
$data['direction'] = $deposit ? 1 : 0;
|
||||
$data['withdraw_id'] = $Withdraw->id;
|
||||
if ($representative) {
|
||||
$data['product_id'] = $representative->id;
|
||||
$data['product'] = [
|
||||
'id' => $representative->id,
|
||||
'title' => $representative->title,
|
||||
'group_type' => $representative->group_type,
|
||||
'metal_id' => $representative->metal_id,
|
||||
'amount' => $representative->amount,
|
||||
'unit' => $representative->unit,
|
||||
'trade_by_quantity' => $representative->trade_by_quantity,
|
||||
];
|
||||
}
|
||||
app(TahesabController::class)->walletGold($data);
|
||||
} catch (\Exception $e) {
|
||||
report($e);
|
||||
@@ -325,6 +337,18 @@ class WalletController extends Controller
|
||||
try {
|
||||
$data['direction'] = $deposit ? 1 : 0;
|
||||
$data['withdraw_id'] = $Withdraw->id;
|
||||
if ($representative) {
|
||||
$data['product_id'] = $representative->id;
|
||||
$data['product'] = [
|
||||
'id' => $representative->id,
|
||||
'title' => $representative->title,
|
||||
'group_type' => $representative->group_type,
|
||||
'metal_id' => $representative->metal_id,
|
||||
'amount' => $representative->amount,
|
||||
'unit' => $representative->unit,
|
||||
'trade_by_quantity' => $representative->trade_by_quantity,
|
||||
];
|
||||
}
|
||||
app(\App\Http\Controllers\Admin\ZarinController::class)->walletGold($data);
|
||||
} catch (\Exception $e) {
|
||||
report($e);
|
||||
@@ -415,6 +439,9 @@ class WalletController extends Controller
|
||||
$label .= ' (غیرفعال)';
|
||||
}
|
||||
|
||||
$payload = Controller::productUnitPayload($product);
|
||||
$isCountBased = Controller::isCountBasedProduct($payload);
|
||||
|
||||
$targets[] = [
|
||||
'key' => 'product:' . $product->id,
|
||||
'kind' => 'metal',
|
||||
@@ -424,6 +451,7 @@ class WalletController extends Controller
|
||||
'metal_id' => max(1, (int) ($product->metal_id ?? 1)),
|
||||
'uses_main_wallet' => $usesMainWallet,
|
||||
'trade_by_quantity' => (bool) $product->trade_by_quantity,
|
||||
'allows_decimal' => !$isCountBased,
|
||||
'group_type' => (string) ($product->group_type ?? ''),
|
||||
'active' => (bool) $product->active,
|
||||
];
|
||||
@@ -453,7 +481,13 @@ class WalletController extends Controller
|
||||
$data = $request->all();
|
||||
$product = Product::findOrFail($productId);
|
||||
$customer = Customer::findOrFail($data['customer_id']);
|
||||
$deposit = ((float) ($data['value'] ?? 0)) > 0;
|
||||
$value = (float) ($data['value'] ?? 0);
|
||||
$deposit = $value > 0;
|
||||
|
||||
$payload = Controller::productUnitPayload($product);
|
||||
if (Controller::isCountBasedProduct($payload) && abs($value - round($value)) > 0.000001) {
|
||||
throw new UnprocessableEntityHttpException('برای محصول تعدادی فقط عدد صحیح مجاز است');
|
||||
}
|
||||
|
||||
$effectiveProductId = Controller::effectiveProductWalletId($productId);
|
||||
$wallet_interface = new WalletInterface(
|
||||
@@ -504,6 +538,16 @@ class WalletController extends Controller
|
||||
try {
|
||||
$data['direction'] = $deposit ? 1 : 0;
|
||||
$data['withdraw_id'] = $Withdraw->id;
|
||||
$data['product_id'] = $productId;
|
||||
$data['product'] = [
|
||||
'id' => $product->id,
|
||||
'title' => $product->title,
|
||||
'group_type' => $product->group_type,
|
||||
'metal_id' => $product->metal_id,
|
||||
'amount' => $product->amount,
|
||||
'unit' => $product->unit,
|
||||
'trade_by_quantity' => $product->trade_by_quantity,
|
||||
];
|
||||
app(TahesabController::class)->walletGold($data);
|
||||
} catch (\Exception $e) {
|
||||
report($e);
|
||||
@@ -514,6 +558,16 @@ class WalletController extends Controller
|
||||
try {
|
||||
$data['direction'] = $deposit ? 1 : 0;
|
||||
$data['withdraw_id'] = $Withdraw->id;
|
||||
$data['product_id'] = $productId;
|
||||
$data['product'] = [
|
||||
'id' => $product->id,
|
||||
'title' => $product->title,
|
||||
'group_type' => $product->group_type,
|
||||
'metal_id' => $product->metal_id,
|
||||
'amount' => $product->amount,
|
||||
'unit' => $product->unit,
|
||||
'trade_by_quantity' => $product->trade_by_quantity,
|
||||
];
|
||||
app(\App\Http\Controllers\Admin\ZarinController::class)->walletGold($data);
|
||||
} catch (\Exception $e) {
|
||||
report($e);
|
||||
@@ -576,19 +630,20 @@ class WalletController extends Controller
|
||||
{
|
||||
$payload = Controller::productUnitPayload($product);
|
||||
|
||||
if (
|
||||
Controller::usesQuantityTrade($payload)
|
||||
&& !Controller::isWeightBasedProductUnit($payload['unit'] ?? null)
|
||||
) {
|
||||
$unit = trim((string) ($payload['unit'] ?? ''));
|
||||
|
||||
return $unit !== '' ? $unit : 'عدد';
|
||||
// تعدادی: همیشه «عدد» — حتی اگر unit محصول وزن هر قطعه (گرم) باشد
|
||||
if (Controller::isCountBasedProduct($payload)) {
|
||||
return 'عدد';
|
||||
}
|
||||
|
||||
if (Controller::isWeightBasedProductUnit($payload['unit'] ?? null)) {
|
||||
return trim((string) $payload['unit']);
|
||||
}
|
||||
|
||||
$unit = trim((string) ($payload['unit'] ?? ''));
|
||||
if ($unit !== '') {
|
||||
return $unit;
|
||||
}
|
||||
|
||||
return Controller::productStorageWeightUnit($product);
|
||||
}
|
||||
|
||||
@@ -1102,6 +1157,26 @@ class WalletController extends Controller
|
||||
return $withdraw;
|
||||
});
|
||||
|
||||
foreach ($data['wallets'] as $walletRow) {
|
||||
$wallet = Wallet::with('product')->find($walletRow['wallet_id'] ?? 0);
|
||||
$product = $wallet?->product;
|
||||
if (!$product) {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
app(\App\Services\AccountingWalletPush::class)->pushProductWalletGold(
|
||||
$customer,
|
||||
$product,
|
||||
(float) $walletRow['value'],
|
||||
false,
|
||||
(int) $withdraw->id,
|
||||
'برداشت فیزیکی ' . $product->title
|
||||
);
|
||||
} catch (\Throwable $e) {
|
||||
report($e);
|
||||
}
|
||||
}
|
||||
|
||||
$this->broadcastCustomerWalletAssets($customer);
|
||||
|
||||
return response()->json(['id' => $withdraw->id]);
|
||||
|
||||
@@ -8,7 +8,7 @@ use App\Models\Customer;
|
||||
use App\Models\Order;
|
||||
use App\Models\Product;
|
||||
use App\Models\Zarin;
|
||||
use App\Services\ConfigService;
|
||||
use App\Services\AccountingProductDocMapper;
|
||||
use App\Services\ZarinSyncService;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Http\Request;
|
||||
@@ -1088,7 +1088,8 @@ class ZarinController extends Controller
|
||||
'id' => $order->product_id,
|
||||
'metal_id' => $existing->type === 'طلا' ? 1 : 2,
|
||||
],
|
||||
'order' => ['id' => $existing->order_id],
|
||||
'product_id' => $order->product_id ? (int) $order->product_id : null,
|
||||
'order' => ['id' => $existing->order_id, 'product_id' => $order->product_id],
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -1099,6 +1100,12 @@ class ZarinController extends Controller
|
||||
: false;
|
||||
}
|
||||
|
||||
$mapper = app(AccountingProductDocMapper::class);
|
||||
$product = $mapper->resolveProductFromPayload($data);
|
||||
if ($product && $mapper->shouldSkip($product)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$customer = Customer::where('id', $data['customer_id'])->first();
|
||||
|
||||
if ($customer && ($customer->moshtari_code === null)) {
|
||||
@@ -1110,9 +1117,19 @@ class ZarinController extends Controller
|
||||
return false;
|
||||
}
|
||||
|
||||
$amount = $data['amount'];
|
||||
$rawAmount = (float) $data['amount'];
|
||||
$count = $mapper->accountingQty($product, $rawAmount, $data);
|
||||
$vazn = $mapper->accountingWeight($product, $rawAmount, $data);
|
||||
$isBuy = (int) ($data['direction'] ?? 0) === 1;
|
||||
$orderId = $data['order']['id'] ?? ($existing->order_id ?? null);
|
||||
$metalId = (int) ($data['product']['metal_id'] ?? 1);
|
||||
$title = $mapper->productTitle($product);
|
||||
$action = $isBuy ? 'خرید' : 'فروش';
|
||||
$sharh = trim($title !== '' ? "{$action} {$title}" : "{$action} {$mapper->kindLabel($mapper->resolveKind($product))}");
|
||||
$syncAmount = $mapper->isCountDoc($product) ? $count : $vazn;
|
||||
$uidSource = $orderId ?? null;
|
||||
if (!empty($data['line'])) {
|
||||
$uidSource = ($orderId ?? '0') . 'L' . (int) $data['line'];
|
||||
}
|
||||
|
||||
$data['handler'] = 'storeTrade';
|
||||
if ($existing && empty($existing->data)) {
|
||||
@@ -1120,84 +1137,55 @@ class ZarinController extends Controller
|
||||
}
|
||||
$zarin = $existing ?? Zarin::create([
|
||||
'customer_id' => $customer->id,
|
||||
'desc' => $data['direction'] == 1 ? 'خرید آبشده' : 'فروش آبشده',
|
||||
'amount' => $amount,
|
||||
'desc' => $sharh,
|
||||
'amount' => $syncAmount,
|
||||
'order_id' => $orderId,
|
||||
'date' => $data['pay_at'] ?? Carbon::now(),
|
||||
'type' => $metalId == 1 ? 'طلا' : 'نقره',
|
||||
'type' => $mapper->metalLabel($product),
|
||||
'created_at' => Carbon::now(),
|
||||
'data' => json_encode($data, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT),
|
||||
]);
|
||||
|
||||
$isStore = app(ConfigService::class)->isStore();
|
||||
$productId = $this->resolveProductId($data, $existing);
|
||||
|
||||
try {
|
||||
if ($isStore && $productId) {
|
||||
$product = Product::find($productId);
|
||||
if (!$product) {
|
||||
$this->sync()->markFailed($zarin, null, 'محصول برای ثبت سند زرین پیدا نشد');
|
||||
return false;
|
||||
if ($mapper->isCountDoc($product)) {
|
||||
$fi = (float) ($data['price'] ?? 0);
|
||||
if ($count > 0) {
|
||||
if (!empty($data['total_price'])) {
|
||||
$fi = ((float) $data['total_price']) / $count;
|
||||
} elseif ($fi > 0 && abs($vazn - $count) > 0.0001) {
|
||||
$fi = $fi * ($vazn / $count);
|
||||
}
|
||||
}
|
||||
|
||||
$karResult = $this->ensureZarinKarForProduct($product);
|
||||
if (empty($karResult['ok'])) {
|
||||
$this->sync()->markFailed($zarin, $karResult, 'کد کار زرین برای محصول آماده نشد');
|
||||
return false;
|
||||
}
|
||||
|
||||
$params = [
|
||||
'kind' => 'sabtSanadTedadi',
|
||||
'parDar' => ((int) ($data['direction'] ?? 0) === 1) ? -1 : 1,
|
||||
'idHesab' => $customer->moshtari_code,
|
||||
'idKar' => (int) $karResult['idKar'],
|
||||
'fi' => $data['price'] ?? 0,
|
||||
'tedad' => $amount,
|
||||
'sharh' => $data['direction'] == 1 ? 'خرید' : 'فروش',
|
||||
'uid' => $this->zarinUid('30', $orderId),
|
||||
];
|
||||
$result = $this->postZarinCountDoc(
|
||||
$zarin,
|
||||
$data,
|
||||
$customer,
|
||||
$product,
|
||||
$count,
|
||||
$isBuy ? -1 : 1,
|
||||
$fi,
|
||||
$sharh,
|
||||
$this->zarinUid('10', $uidSource ?? $zarin->id)
|
||||
);
|
||||
} else {
|
||||
$params = [
|
||||
'kind' => 'pooli',
|
||||
'radif' => 1,
|
||||
'korf' => ((int) ($data['direction'] ?? 0) === 1) ? 'f' : 'k',
|
||||
'poolorvazn' => 'vazn',
|
||||
'meghdar' => $amount,
|
||||
'fi' => $data['price'] ?? 0,
|
||||
'hesab' => $customer->moshtari_code,
|
||||
'sharh' => $data['direction'] == 1 ? 'خرید آبشده' : 'فروش آبشده',
|
||||
'ayar' => '750',
|
||||
'movaghat' => '',
|
||||
'idFactor' => '',
|
||||
'uid' => $this->zarinUid('10', $orderId),
|
||||
];
|
||||
}
|
||||
|
||||
$http = $this->zarinRequest($params);
|
||||
if (!$http) {
|
||||
$this->sync()->markFailed($zarin, null, 'تنظیمات زرین کامل نیست');
|
||||
return 'تنظیمات زرین کامل نیست. zarin_api یا zarin_token تنظیم نشده است.';
|
||||
}
|
||||
|
||||
[$response, $httpError] = $this->zarinDecodeResponse($http);
|
||||
if ($httpError) {
|
||||
$this->sync()->markFailed($zarin, $response ?? $httpError, $httpError);
|
||||
return false;
|
||||
$result = $this->postZarinWeightDoc(
|
||||
$zarin,
|
||||
$data,
|
||||
$customer,
|
||||
$vazn,
|
||||
$isBuy ? 'f' : 'k',
|
||||
(float) ($data['price'] ?? 0),
|
||||
$sharh,
|
||||
$mapper->jensFelez($product) ?? 0,
|
||||
$this->zarinUid('10', $uidSource ?? $zarin->id)
|
||||
);
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
$this->sync()->markFailed($zarin, $e->getMessage());
|
||||
return $e->getMessage();
|
||||
}
|
||||
|
||||
$flat = $this->extractResponseRow(is_array($response) ? $response : null);
|
||||
|
||||
if ($this->sync()->responseOk(is_array($response) ? $response : []) || $this->sync()->responseOk($flat)) {
|
||||
$this->sync()->markSuccess($zarin, is_array($response) ? $response : $flat, $data);
|
||||
return $data;
|
||||
}
|
||||
|
||||
$this->sync()->markFailed($zarin, $response, 'ثبت سند معامله در زرین ناموفق بود');
|
||||
return false;
|
||||
return $result;
|
||||
}
|
||||
|
||||
// direction: 0 = daryaft az moshtari, 1 = pardakht be moshtari
|
||||
@@ -1413,7 +1401,7 @@ class ZarinController extends Controller
|
||||
return false;
|
||||
}
|
||||
|
||||
// direction: 1 = واریز طلا (خرید)، 0 = برداشت طلا (فروش)
|
||||
// direction: 1 = واریز، 0 = برداشت
|
||||
public function walletGold($data, ?Zarin $existing = null)
|
||||
{
|
||||
if (empty($data['customer_id']) && $existing) {
|
||||
@@ -1423,6 +1411,15 @@ class ZarinController extends Controller
|
||||
'direction' => str_contains((string) $existing->desc, 'برداشت') ? 0 : 1,
|
||||
'withdraw_id' => $existing->withdraw_id,
|
||||
]);
|
||||
$payload = json_decode((string) ($existing->data ?? ''), true);
|
||||
if (is_array($payload)) {
|
||||
if (!empty($payload['product_id'])) {
|
||||
$data['product_id'] = (int) $payload['product_id'];
|
||||
}
|
||||
if (!empty($payload['product']) && is_array($payload['product'])) {
|
||||
$data['product'] = $payload['product'];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!isset($data['customer_id'], $data['value'])) {
|
||||
@@ -1431,6 +1428,12 @@ class ZarinController extends Controller
|
||||
: false;
|
||||
}
|
||||
|
||||
$mapper = app(AccountingProductDocMapper::class);
|
||||
$product = $mapper->resolveProductFromPayload($data);
|
||||
if ($product && $mapper->shouldSkip($product)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$customer = Customer::where('id', $data['customer_id'])->first();
|
||||
|
||||
if ($customer && ($customer->moshtari_code === null)) {
|
||||
@@ -1442,16 +1445,27 @@ class ZarinController extends Controller
|
||||
return false;
|
||||
}
|
||||
|
||||
$amount = abs($data['value']);
|
||||
$rawValue = (float) $data['value'];
|
||||
$count = $mapper->accountingQty($product, $rawValue, $data);
|
||||
$vazn = $mapper->accountingWeight($product, $rawValue, $data);
|
||||
$isDeposit = (int) ($data['direction'] ?? 1) === 1;
|
||||
|
||||
$title = $mapper->productTitle($product);
|
||||
$action = $isDeposit ? 'شارژ' : 'دشارژ';
|
||||
$sharh = !empty($data['note'])
|
||||
? (string) $data['note']
|
||||
: (!empty($data['desc'])
|
||||
? (string) $data['desc']
|
||||
: trim($title !== '' ? "{$action} {$title}" : "{$action} {$mapper->kindLabel($mapper->resolveKind($product))}"));
|
||||
$data['note'] = $sharh;
|
||||
$data['handler'] = 'walletGold';
|
||||
$syncAmount = $mapper->isCountDoc($product) ? $count : $vazn;
|
||||
|
||||
$zarin = $existing ?? Zarin::create([
|
||||
'customer_id' => $customer->id,
|
||||
'desc' => $isDeposit ? 'شارژ کیف پول آبشده' : 'برداشت از کیف پول آبشده',
|
||||
'amount' => $amount,
|
||||
'desc' => $sharh,
|
||||
'amount' => $syncAmount,
|
||||
'date' => Carbon::now(),
|
||||
'type' => 'طلا',
|
||||
'type' => $mapper->metalLabel($product),
|
||||
'created_at' => Carbon::now(),
|
||||
'withdraw_id' => $data['withdraw_id'] ?? null,
|
||||
'data' => json_encode($data, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT),
|
||||
@@ -1467,50 +1481,160 @@ class ZarinController extends Controller
|
||||
$mazaneh = 1;
|
||||
}
|
||||
|
||||
$sharh = !empty($data['note'])
|
||||
? $data['note']
|
||||
: (!empty($data['desc']) ? $data['desc'] : ($isDeposit ? 'خرید آبشده' : 'فروش آبشده'));
|
||||
$data['note'] = $sharh;
|
||||
|
||||
$uidSource = $data['withdraw_id'] ?? $zarin->id;
|
||||
|
||||
try {
|
||||
$http = $this->zarinRequest([
|
||||
'kind' => 'pooli',
|
||||
'radif' => 1,
|
||||
'korf' => $isDeposit ? 'f' : 'k',
|
||||
'poolorvazn' => 'vazn',
|
||||
'meghdar' => $amount,
|
||||
'fi' => $mazaneh,
|
||||
'hesab' => $customer->moshtari_code,
|
||||
'sharh' => $sharh,
|
||||
'ayar' => '750',
|
||||
'movaghat' => '',
|
||||
'idFactor' => '',
|
||||
'uid' => $this->zarinUid('21', $uidSource),
|
||||
]);
|
||||
if ($mapper->isCountDoc($product)) {
|
||||
$unitPrice = $mazaneh;
|
||||
if ($product && (float) ($product->amount ?? 0) > 0) {
|
||||
$unitPrice = (int) round($mazaneh * (float) $product->amount);
|
||||
}
|
||||
if ($unitPrice <= 0) {
|
||||
$unitPrice = $mazaneh;
|
||||
}
|
||||
|
||||
if (!$http) {
|
||||
$this->sync()->markFailed($zarin, null, 'تنظیمات زرین کامل نیست');
|
||||
return 'تنظیمات زرین کامل نیست. zarin_api یا zarin_token تنظیم نشده است.';
|
||||
return $this->postZarinCountDoc(
|
||||
$zarin,
|
||||
$data,
|
||||
$customer,
|
||||
$product,
|
||||
$count,
|
||||
$isDeposit ? -1 : 1,
|
||||
(float) $unitPrice,
|
||||
$sharh,
|
||||
$this->zarinUid('21', $uidSource)
|
||||
);
|
||||
}
|
||||
|
||||
[$response, $httpError] = $this->zarinDecodeResponse($http);
|
||||
if ($httpError) {
|
||||
$this->sync()->markFailed($zarin, $response ?? $httpError, $httpError);
|
||||
return false;
|
||||
}
|
||||
return $this->postZarinWeightDoc(
|
||||
$zarin,
|
||||
$data,
|
||||
$customer,
|
||||
$vazn,
|
||||
$isDeposit ? 'f' : 'k',
|
||||
(float) $mazaneh,
|
||||
$sharh,
|
||||
$mapper->jensFelez($product) ?? 0,
|
||||
$this->zarinUid('21', $uidSource)
|
||||
);
|
||||
} catch (\Exception $e) {
|
||||
$this->sync()->markFailed($zarin, $e->getMessage());
|
||||
return $e->getMessage();
|
||||
}
|
||||
}
|
||||
|
||||
if ($this->sync()->responseOk(is_array($response) ? $response : [])) {
|
||||
$this->sync()->markSuccess($zarin, is_array($response) ? $response : [], $data);
|
||||
/**
|
||||
* سند تعدادی زرین (سکه / شمش / گوی / مکعب).
|
||||
*/
|
||||
private function postZarinCountDoc(
|
||||
Zarin $zarin,
|
||||
array $data,
|
||||
Customer $customer,
|
||||
$product,
|
||||
float $tedad,
|
||||
int $parDar,
|
||||
float $fi,
|
||||
string $sharh,
|
||||
string $uid
|
||||
) {
|
||||
if (!$product instanceof Product || empty($product->id)) {
|
||||
$this->sync()->markFailed($zarin, null, 'محصول برای سند تعدادی زرین مشخص نیست');
|
||||
return false;
|
||||
}
|
||||
|
||||
$kar = $this->ensureZarinKarForProduct($product);
|
||||
if (empty($kar['ok']) || empty($kar['idKar'])) {
|
||||
$this->sync()->markFailed($zarin, $kar ?? null, $kar['message'] ?? 'کد کار زرین برای محصول یافت نشد');
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($fi <= 0) {
|
||||
$fi = 1;
|
||||
}
|
||||
|
||||
$http = $this->zarinRequest([
|
||||
'kind' => 'sabtSanadTedadi',
|
||||
'idHesab' => $customer->moshtari_code,
|
||||
'idKar' => (int) $kar['idKar'],
|
||||
'fi' => $fi,
|
||||
'tedad' => $tedad,
|
||||
'parDar' => $parDar,
|
||||
'sharh' => $sharh,
|
||||
'uid' => $uid,
|
||||
]);
|
||||
|
||||
if (!$http) {
|
||||
$this->sync()->markFailed($zarin, null, 'تنظیمات زرین کامل نیست');
|
||||
return 'تنظیمات زرین کامل نیست. zarin_api یا zarin_token تنظیم نشده است.';
|
||||
}
|
||||
|
||||
[$response, $httpError] = $this->zarinDecodeResponse($http);
|
||||
if ($httpError) {
|
||||
$this->sync()->markFailed($zarin, $response ?? $httpError, $httpError);
|
||||
return false;
|
||||
}
|
||||
|
||||
$flat = $this->extractResponseRow(is_array($response) ? $response : null);
|
||||
if ($this->sync()->responseOk(is_array($response) ? $response : []) || $this->sync()->responseOk($flat)) {
|
||||
$this->sync()->markSuccess($zarin, is_array($response) ? $response : $flat, $data);
|
||||
return $data;
|
||||
}
|
||||
|
||||
$this->sync()->markFailed($zarin, $response, 'ثبت سند طلایی کیف پول در زرین ناموفق بود');
|
||||
$this->sync()->markFailed($zarin, $response, 'ثبت سند تعدادی در زرین ناموفق بود');
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* سند وزنی زرین (آبشده طلا/نقره).
|
||||
*/
|
||||
private function postZarinWeightDoc(
|
||||
Zarin $zarin,
|
||||
array $data,
|
||||
Customer $customer,
|
||||
float $meghdar,
|
||||
string $korf,
|
||||
float $fi,
|
||||
string $sharh,
|
||||
int $jensFelez,
|
||||
string $uid
|
||||
) {
|
||||
if ($fi <= 0) {
|
||||
$fi = 1;
|
||||
}
|
||||
|
||||
$http = $this->zarinRequest([
|
||||
'kind' => 'pooli',
|
||||
'radif' => 1,
|
||||
'korf' => $korf,
|
||||
'poolorvazn' => 'vazn',
|
||||
'meghdar' => $meghdar,
|
||||
'fi' => $fi,
|
||||
'hesab' => $customer->moshtari_code,
|
||||
'sharh' => $sharh,
|
||||
'ayar' => $jensFelez === 2 ? '' : '750',
|
||||
'movaghat' => '',
|
||||
'idFactor' => '',
|
||||
'uid' => $uid,
|
||||
]);
|
||||
|
||||
if (!$http) {
|
||||
$this->sync()->markFailed($zarin, null, 'تنظیمات زرین کامل نیست');
|
||||
return 'تنظیمات زرین کامل نیست. zarin_api یا zarin_token تنظیم نشده است.';
|
||||
}
|
||||
|
||||
[$response, $httpError] = $this->zarinDecodeResponse($http);
|
||||
if ($httpError) {
|
||||
$this->sync()->markFailed($zarin, $response ?? $httpError, $httpError);
|
||||
return false;
|
||||
}
|
||||
|
||||
$flat = $this->extractResponseRow(is_array($response) ? $response : null);
|
||||
if ($this->sync()->responseOk(is_array($response) ? $response : []) || $this->sync()->responseOk($flat)) {
|
||||
$this->sync()->markSuccess($zarin, is_array($response) ? $response : $flat, $data);
|
||||
return $data;
|
||||
}
|
||||
|
||||
$this->sync()->markFailed($zarin, $response, 'ثبت سند وزنی در زرین ناموفق بود');
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -353,6 +353,7 @@ class CartController extends Controller
|
||||
|
||||
/**
|
||||
* ثبت سبد در دفترکل داخلی + حسابداری بیرونی (مستقل از is_store؛ فقط accounting_provider).
|
||||
* تهحساب/زرین: per-line تا دستهٔ هر محصول درست برود.
|
||||
*/
|
||||
private function syncCartOrderAccounting(Order $order, Cart $cart): void
|
||||
{
|
||||
@@ -364,43 +365,62 @@ class CartController extends Controller
|
||||
report($e);
|
||||
}
|
||||
|
||||
$primaryProduct = Product::find($cart->items->first()?->product_id);
|
||||
$metalId = max(1, (int) ($primaryProduct->metal_id ?? 1));
|
||||
|
||||
if ($config->isTahesabAccounting()) {
|
||||
try {
|
||||
app(TahesabController::class)->storeTrade([
|
||||
'customer_id' => $order->customer_id,
|
||||
'amount' => $order->amount,
|
||||
'direction' => (int) ($order->direction ?? 1),
|
||||
'price' => $order->price,
|
||||
'total_price' => $order->total_price,
|
||||
'pay_at' => $order->pay_at ?? $order->created_at,
|
||||
'product' => ['metal_id' => $metalId],
|
||||
'order' => ['id' => $order->id],
|
||||
]);
|
||||
} catch (\Throwable $e) {
|
||||
report($e);
|
||||
$lineIndex = 0;
|
||||
foreach ($cart->items as $item) {
|
||||
$product = Product::find($item->product_id);
|
||||
if (!$product) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if ($config->isZarinAccounting()) {
|
||||
try {
|
||||
app(\App\Http\Controllers\Admin\ZarinController::class)->storeTrade([
|
||||
'customer_id' => $order->customer_id,
|
||||
'amount' => $order->amount,
|
||||
'direction' => (int) ($order->direction ?? 1),
|
||||
'price' => $order->price,
|
||||
'total_price' => $order->total_price,
|
||||
'pay_at' => $order->pay_at ?? $order->created_at,
|
||||
'product' => [
|
||||
'metal_id' => $metalId,
|
||||
'id' => $primaryProduct?->id,
|
||||
],
|
||||
'order' => ['id' => $order->id],
|
||||
]);
|
||||
} catch (\Throwable $e) {
|
||||
report($e);
|
||||
$qty = max(1, (int) ($item->quantity ?? 1));
|
||||
$unitWeight = (float) ($product->amount ?? 0);
|
||||
$lineWeight = $unitWeight > 0 ? $unitWeight * $qty : (float) $qty;
|
||||
$unitPrice = (float) ($item->unit_price ?? 0);
|
||||
if ($unitPrice <= 0) {
|
||||
$unitPrice = (float) ($product->price ?? 0) + (float) ($product->price_profit ?? 0);
|
||||
}
|
||||
$lineTotal = (float) ($item->total_price ?? ($unitPrice * $qty));
|
||||
|
||||
$payload = [
|
||||
'customer_id' => $order->customer_id,
|
||||
'amount' => $lineWeight,
|
||||
'number' => $qty,
|
||||
'direction' => (int) ($order->direction ?? 1),
|
||||
'price' => $unitPrice,
|
||||
'total_price' => $lineTotal,
|
||||
'pay_at' => $order->pay_at ?? $order->created_at,
|
||||
'product_id' => (int) $product->id,
|
||||
'product' => [
|
||||
'id' => $product->id,
|
||||
'title' => $product->title,
|
||||
'group_type' => $product->group_type,
|
||||
'metal_id' => $product->metal_id,
|
||||
'amount' => $product->amount,
|
||||
'unit' => $product->unit,
|
||||
'trade_by_quantity' => $product->trade_by_quantity,
|
||||
],
|
||||
'line' => $lineIndex + 1,
|
||||
'order' => [
|
||||
'id' => (int) $order->id,
|
||||
'product_id' => (int) $product->id,
|
||||
],
|
||||
];
|
||||
$lineIndex++;
|
||||
|
||||
if ($config->isTahesabAccounting()) {
|
||||
try {
|
||||
app(TahesabController::class)->storeTrade($payload);
|
||||
} catch (\Throwable $e) {
|
||||
report($e);
|
||||
}
|
||||
}
|
||||
|
||||
if ($config->isZarinAccounting()) {
|
||||
try {
|
||||
app(\App\Http\Controllers\Admin\ZarinController::class)->storeTrade($payload);
|
||||
} catch (\Throwable $e) {
|
||||
report($e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -593,6 +593,29 @@ class WalletController extends Controller
|
||||
], 6, false);
|
||||
}
|
||||
|
||||
foreach ($data['wallets'] as $walletRow) {
|
||||
$product = $walletRow['wallet']['product'] ?? null;
|
||||
if (!$product) {
|
||||
continue;
|
||||
}
|
||||
$productModel = $product instanceof Product ? $product : Product::find($product['id'] ?? 0);
|
||||
if (!$productModel) {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
app(\App\Services\AccountingWalletPush::class)->pushProductWalletGold(
|
||||
$data['customer'],
|
||||
$productModel,
|
||||
(float) $walletRow['value'],
|
||||
false,
|
||||
(int) $data['withdraw']->id,
|
||||
'برداشت فیزیکی ' . $productModel->title
|
||||
);
|
||||
} catch (\Throwable $e) {
|
||||
report($e);
|
||||
}
|
||||
}
|
||||
|
||||
return response()->json($data['withdraw']['id']);
|
||||
}
|
||||
public function indexWithdraw(Request $request){
|
||||
|
||||
@@ -0,0 +1,274 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Models\Product;
|
||||
|
||||
/**
|
||||
* نگاشت محصول → نوع سند تهحساب/زرین بر اساس group_type و metal_id.
|
||||
* مس و فلز ناشناخته → skip (بدون ارسال سند).
|
||||
*/
|
||||
class AccountingProductDocMapper
|
||||
{
|
||||
public const KIND_SEKEH = 'sekeh';
|
||||
public const KIND_SHEMSH = 'shemsh';
|
||||
public const KIND_KAR = 'kar';
|
||||
public const KIND_WEIGHT = 'weight';
|
||||
public const KIND_SKIP = 'skip';
|
||||
|
||||
private const SEKEH_GROUPS = ['سکه', 'سکه پارسیان'];
|
||||
private const SHEMSH_GROUPS = ['شمش', 'شمش برنا', 'شمش ونوس'];
|
||||
private const KAR_GROUPS = ['مکعب', 'گوی'];
|
||||
private const SKIP_GROUPS = ['مس', 'مفتول مس'];
|
||||
|
||||
/**
|
||||
* @param Product|array|null $product
|
||||
*/
|
||||
public function resolveKind($product): string
|
||||
{
|
||||
if (!$product) {
|
||||
return self::KIND_WEIGHT;
|
||||
}
|
||||
|
||||
$group = $this->groupType($product);
|
||||
$metalId = $this->metalId($product);
|
||||
|
||||
if ($group !== '' && in_array($group, self::SKIP_GROUPS, true)) {
|
||||
return self::KIND_SKIP;
|
||||
}
|
||||
if ($metalId === 3) {
|
||||
return self::KIND_SKIP;
|
||||
}
|
||||
|
||||
if (in_array($group, self::SEKEH_GROUPS, true)) {
|
||||
return self::KIND_SEKEH;
|
||||
}
|
||||
if (in_array($group, self::SHEMSH_GROUPS, true)) {
|
||||
return self::KIND_SHEMSH;
|
||||
}
|
||||
if (in_array($group, self::KAR_GROUPS, true)) {
|
||||
return self::KIND_KAR;
|
||||
}
|
||||
|
||||
// آبشده / کاتد / مقداری وزنی / نقره دستهای → سند وزنی (فلز با Jens_Felez جدا میشود)
|
||||
return self::KIND_WEIGHT;
|
||||
}
|
||||
|
||||
public function isCountDoc($product): bool
|
||||
{
|
||||
$kind = $this->resolveKind($product);
|
||||
|
||||
return in_array($kind, [self::KIND_SEKEH, self::KIND_SHEMSH, self::KIND_KAR], true);
|
||||
}
|
||||
|
||||
public function shouldSkip($product): bool
|
||||
{
|
||||
return $this->resolveKind($product) === self::KIND_SKIP;
|
||||
}
|
||||
|
||||
/**
|
||||
* Jens_Felez تهحساب: 0=طلا، 2=نقره. null = skip (مثلاً مس).
|
||||
*
|
||||
* @param Product|array|null $product
|
||||
*/
|
||||
public function jensFelez($product): ?int
|
||||
{
|
||||
if ($this->shouldSkip($product)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$metalId = $this->metalId($product);
|
||||
if ($metalId === 2) {
|
||||
return 2;
|
||||
}
|
||||
if ($metalId === 3) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* مقدار تعداد برای سند تعدادی.
|
||||
* ترجیح: number/quantity/count از payload؛ وگرنه rawValue (شارژ کیف = تعداد).
|
||||
*
|
||||
* @param Product|array|null $product
|
||||
* @param array<string,mixed> $payload
|
||||
*/
|
||||
public function accountingQty($product, float $rawValue, array $payload = []): float
|
||||
{
|
||||
if (!$this->isCountDoc($product)) {
|
||||
return abs($rawValue);
|
||||
}
|
||||
|
||||
foreach (['number', 'quantity', 'count', 'tedad'] as $key) {
|
||||
if (isset($payload[$key]) && is_numeric($payload[$key]) && (float) $payload[$key] != 0.0) {
|
||||
return abs((float) $payload[$key]);
|
||||
}
|
||||
}
|
||||
|
||||
// سفارشهای قدیمی: amount = number × وزن واحد → تعداد را از وزن برگردان
|
||||
$unitWeight = $this->unitWeight($product);
|
||||
if ($unitWeight > 0 && abs($rawValue) > $unitWeight + 0.000001) {
|
||||
$derived = abs($rawValue) / $unitWeight;
|
||||
if (abs($derived - round($derived)) < 0.0001) {
|
||||
return round($derived);
|
||||
}
|
||||
}
|
||||
|
||||
return abs($rawValue);
|
||||
}
|
||||
|
||||
/**
|
||||
* وزن برای سند: تعدادی → count × amount؛ وزنی → همان مقدار.
|
||||
* اگر rawValue از قبل وزن سفارش باشد (number×amount)، همان را برگردان.
|
||||
*
|
||||
* @param Product|array|null $product
|
||||
* @param array<string,mixed> $payload
|
||||
*/
|
||||
public function accountingWeight($product, float $rawValue, array $payload = []): float
|
||||
{
|
||||
$qty = abs($rawValue);
|
||||
if (!$this->isCountDoc($product)) {
|
||||
return $qty;
|
||||
}
|
||||
|
||||
$count = $this->accountingQty($product, $rawValue, $payload);
|
||||
$unitWeight = $this->unitWeight($product);
|
||||
if ($unitWeight > 0) {
|
||||
// اگر rawValue تقریباً همان count×unit است، دوباره ضرب نکن
|
||||
$expected = round($count * $unitWeight, 6);
|
||||
if (abs($qty - $expected) < 0.0001 || abs($qty - $count) < 0.0001) {
|
||||
return $expected;
|
||||
}
|
||||
// rawValue احتمالاً همان وزن سفارش است
|
||||
if (abs($qty - $count * $unitWeight) < max(0.01, $unitWeight * 0.01)) {
|
||||
return round($qty, 6);
|
||||
}
|
||||
|
||||
return $expected;
|
||||
}
|
||||
|
||||
return $qty;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Product|array|null $product
|
||||
*/
|
||||
public function productTitle($product): string
|
||||
{
|
||||
if (is_object($product)) {
|
||||
return trim((string) ($product->title ?? ''));
|
||||
}
|
||||
if (is_array($product)) {
|
||||
return trim((string) ($product['title'] ?? ''));
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* برچسب نوع سند برای desc جدول sync.
|
||||
*/
|
||||
public function kindLabel(string $kind): string
|
||||
{
|
||||
return match ($kind) {
|
||||
self::KIND_SEKEH => 'سکه',
|
||||
self::KIND_SHEMSH => 'شمش',
|
||||
self::KIND_KAR => 'کارساخته',
|
||||
self::KIND_WEIGHT => 'وزنی',
|
||||
default => 'ردشده',
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Product|array|null $product
|
||||
*/
|
||||
public function metalLabel($product): string
|
||||
{
|
||||
$jens = $this->jensFelez($product);
|
||||
if ($jens === 2) {
|
||||
return 'نقره';
|
||||
}
|
||||
|
||||
return 'طلا';
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve Product model from storeTrade/walletGold payload.
|
||||
*/
|
||||
public function resolveProductFromPayload(array $data): ?Product
|
||||
{
|
||||
$productId = 0;
|
||||
if (!empty($data['product_id'])) {
|
||||
$productId = (int) $data['product_id'];
|
||||
} elseif (!empty($data['product']['id'])) {
|
||||
$productId = (int) $data['product']['id'];
|
||||
} elseif (!empty($data['order']['product_id'])) {
|
||||
$productId = (int) $data['order']['product_id'];
|
||||
}
|
||||
|
||||
if ($productId > 0) {
|
||||
return Product::query()->withTrashed()->find($productId);
|
||||
}
|
||||
|
||||
// فقط metal_id در payload (retry قدیمی) — بدون group_type → وزن
|
||||
if (isset($data['product']) && is_array($data['product'])) {
|
||||
return new Product([
|
||||
'title' => (string) ($data['product']['title'] ?? ''),
|
||||
'group_type' => (string) ($data['product']['group_type'] ?? 'آبشده'),
|
||||
'metal_id' => (int) ($data['product']['metal_id'] ?? 1),
|
||||
'amount' => (float) ($data['product']['amount'] ?? 0),
|
||||
'unit' => (string) ($data['product']['unit'] ?? 'گرم'),
|
||||
'trade_by_quantity' => (bool) ($data['product']['trade_by_quantity'] ?? false),
|
||||
]);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Product|array|null $product
|
||||
*/
|
||||
private function groupType($product): string
|
||||
{
|
||||
if (is_object($product)) {
|
||||
return trim((string) ($product->group_type ?? ''));
|
||||
}
|
||||
if (is_array($product)) {
|
||||
return trim((string) ($product['group_type'] ?? ''));
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Product|array|null $product
|
||||
*/
|
||||
private function metalId($product): int
|
||||
{
|
||||
if (is_object($product)) {
|
||||
return max(1, (int) ($product->metal_id ?? 1));
|
||||
}
|
||||
if (is_array($product)) {
|
||||
return max(1, (int) ($product['metal_id'] ?? 1));
|
||||
}
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Product|array|null $product
|
||||
*/
|
||||
private function unitWeight($product): float
|
||||
{
|
||||
if (is_object($product)) {
|
||||
return (float) ($product->amount ?? 0);
|
||||
}
|
||||
if (is_array($product)) {
|
||||
return (float) ($product['amount'] ?? 0);
|
||||
}
|
||||
|
||||
return 0.0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Http\Controllers\Admin\TahesabController;
|
||||
use App\Http\Controllers\Admin\ZarinController;
|
||||
use App\Models\Customer;
|
||||
use App\Models\Product;
|
||||
|
||||
/**
|
||||
* ارسال سند کیف محصول به تهحساب/زرین (بدون پرگار).
|
||||
*/
|
||||
class AccountingWalletPush
|
||||
{
|
||||
public function pushProductWalletGold(
|
||||
Customer $customer,
|
||||
Product $product,
|
||||
float $value,
|
||||
bool $isDeposit,
|
||||
?int $withdrawId = null,
|
||||
?string $note = null
|
||||
): void {
|
||||
$mapper = app(AccountingProductDocMapper::class);
|
||||
if ($mapper->shouldSkip($product)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$config = app(ConfigService::class);
|
||||
$payload = [
|
||||
'customer_id' => $customer->id,
|
||||
'value' => abs($value),
|
||||
'direction' => $isDeposit ? 1 : 0,
|
||||
'withdraw_id' => $withdrawId,
|
||||
'note' => $note,
|
||||
'product_id' => (int) $product->id,
|
||||
'product' => [
|
||||
'id' => $product->id,
|
||||
'title' => $product->title,
|
||||
'group_type' => $product->group_type,
|
||||
'metal_id' => $product->metal_id,
|
||||
'amount' => $product->amount,
|
||||
'unit' => $product->unit,
|
||||
'trade_by_quantity' => $product->trade_by_quantity,
|
||||
],
|
||||
];
|
||||
|
||||
if ($config->isTahesabAccounting()) {
|
||||
try {
|
||||
app(TahesabController::class)->walletGold($payload);
|
||||
} catch (\Throwable $e) {
|
||||
report($e);
|
||||
}
|
||||
}
|
||||
|
||||
if ($config->isZarinAccounting()) {
|
||||
try {
|
||||
app(ZarinController::class)->walletGold($payload);
|
||||
} catch (\Throwable $e) {
|
||||
report($e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user