296 lines
8.8 KiB
PHP
296 lines
8.8 KiB
PHP
<?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 'طلا';
|
|
}
|
|
|
|
/**
|
|
* برچسب ستون type جدول sync (تهحساب / زرین):
|
|
* تعدادی → سکه / شمش / کارساخته؛ وزنی → طلا / نقره.
|
|
*
|
|
* @param Product|array|null $product
|
|
*/
|
|
public function syncTypeLabel($product): string
|
|
{
|
|
if ($this->isCountDoc($product)) {
|
|
return $this->kindLabel($this->resolveKind($product));
|
|
}
|
|
|
|
return $this->metalLabel($product);
|
|
}
|
|
|
|
/** @deprecated از syncTypeLabel استفاده کنید */
|
|
public function zarinSyncTypeLabel($product): string
|
|
{
|
|
return $this->syncTypeLabel($product);
|
|
}
|
|
|
|
/**
|
|
* 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;
|
|
}
|
|
}
|