feat(accounting): document colleague trades on configured accounts
Post Montzar-covered buy/sell to Zarin/Tahesab/Pargar using montzar_account_* settings, and harden public storage URL/MIME handling for uploads. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -40,9 +40,7 @@ class FileController extends Controller
|
||||
|
||||
$path = $this->createDir();
|
||||
$originalName = $uploaded->getClientOriginalName();
|
||||
$ext = $uploaded->getClientOriginalExtension()
|
||||
?: $uploaded->extension()
|
||||
?: 'jpg';
|
||||
$ext = $this->resolveUploadExtension($uploaded);
|
||||
$title = $request->input('title') ?: $originalName;
|
||||
$file_name_src = md5(time().$originalName);
|
||||
$file_name = $file_name_src.'.'.$ext;
|
||||
@@ -70,4 +68,35 @@ class FileController extends Controller
|
||||
|
||||
return response()->json($payload);
|
||||
}
|
||||
|
||||
/**
|
||||
* پسوند واقعی فایل (MIME)، نه اسم اصلی مرورگر — آپلود بنر WebP است.
|
||||
*/
|
||||
private function resolveUploadExtension(\Illuminate\Http\UploadedFile $uploaded): string
|
||||
{
|
||||
$mime = strtolower((string) ($uploaded->getMimeType() ?: $uploaded->getClientMimeType()));
|
||||
$fromMime = match (true) {
|
||||
str_contains($mime, 'webp') => 'webp',
|
||||
str_contains($mime, 'png') => 'png',
|
||||
str_contains($mime, 'jpeg'), str_contains($mime, 'jpg') => 'jpg',
|
||||
str_contains($mime, 'gif') => 'gif',
|
||||
str_contains($mime, 'svg') => 'svg',
|
||||
default => null,
|
||||
};
|
||||
if ($fromMime) {
|
||||
return $fromMime;
|
||||
}
|
||||
|
||||
$client = strtolower((string) $uploaded->getClientOriginalExtension());
|
||||
if (in_array($client, ['webp', 'png', 'jpg', 'jpeg', 'gif', 'svg'], true)) {
|
||||
return $client === 'jpeg' ? 'jpg' : $client;
|
||||
}
|
||||
|
||||
$guessed = strtolower((string) $uploaded->extension());
|
||||
if (in_array($guessed, ['webp', 'png', 'jpg', 'jpeg', 'gif', 'svg'], true)) {
|
||||
return $guessed === 'jpeg' ? 'jpg' : $guessed;
|
||||
}
|
||||
|
||||
return 'jpg';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -453,10 +453,25 @@ class TahesabController extends Controller
|
||||
}
|
||||
|
||||
$customer = Customer::where('id', $data['customer_id'])->first();
|
||||
$accountOverride = trim((string) ($data['accounting_account_code'] ?? ''));
|
||||
$isColleagueLeg = ! empty($data['colleague_leg']) && $accountOverride !== '';
|
||||
|
||||
if ($customer && ($customer->moshtari_code === null)) {
|
||||
$this->createCustomerInTahesab($customer->id);
|
||||
$customer = Customer::where('id', $data['customer_id'])->first();
|
||||
if (! $isColleagueLeg) {
|
||||
if ($customer && ($customer->moshtari_code === null)) {
|
||||
$this->createCustomerInTahesab($customer->id);
|
||||
$customer = Customer::where('id', $data['customer_id'])->first();
|
||||
}
|
||||
}
|
||||
|
||||
if (! $customer) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$hesabCode = $isColleagueLeg
|
||||
? (int) $accountOverride
|
||||
: (int) ($customer->moshtari_code ?? 0);
|
||||
if ($hesabCode <= 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$kind = $mapper->resolveKind($product);
|
||||
@@ -490,7 +505,14 @@ class TahesabController extends Controller
|
||||
|
||||
$action = $isBuy ? 'خرید' : 'فروش';
|
||||
$kindFa = $mapper->kindLabel($kind);
|
||||
$sharh = trim($title !== '' ? "{$action} {$title}" : "{$action} {$kindFa}");
|
||||
$sharh = trim((string) ($data['sharh_override'] ?? ''));
|
||||
if ($sharh === '') {
|
||||
$sharh = trim($title !== '' ? "{$action} {$title}" : "{$action} {$kindFa}");
|
||||
}
|
||||
|
||||
$orderId = is_object($data['order'] ?? null)
|
||||
? ($data['order']->id ?? null)
|
||||
: ($data['order']['id'] ?? null);
|
||||
|
||||
$data['handler'] = 'storeTrade';
|
||||
if ($existing && empty($existing->data)) {
|
||||
@@ -500,21 +522,21 @@ class TahesabController extends Controller
|
||||
'customer_id' => $customer->id,
|
||||
'desc' => $sharh,
|
||||
'amount' => $mapper->isCountDoc($product) ? $count : $vazn,
|
||||
'order_id' => $data['order']['id'] ?? null,
|
||||
'order_id' => $orderId,
|
||||
'date' => $data['pay_at'] ?? Carbon::now(),
|
||||
'type' => $mapper->syncTypeLabel($product),
|
||||
'created_at' => Carbon::now(),
|
||||
'data' => json_encode($data, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT),
|
||||
]);
|
||||
|
||||
if ($mapper->isCountDoc($product) && $title === '') {
|
||||
if ($mapper->isCountDoc($product) && $title === '' && ! $isColleagueLeg) {
|
||||
$this->sync()->markFailed($tahesab, null, 'نام محصول برای سند تعدادی تهحساب خالی است');
|
||||
return false;
|
||||
}
|
||||
|
||||
$body = $this->buildBuySaleBody(
|
||||
$kind,
|
||||
(int) $customer->moshtari_code,
|
||||
$hesabCode,
|
||||
(int) $year,
|
||||
(int) $month,
|
||||
(int) $day,
|
||||
|
||||
@@ -1394,29 +1394,49 @@ class ZarinController extends Controller
|
||||
}
|
||||
|
||||
$customer = Customer::where('id', $data['customer_id'])->first();
|
||||
$accountOverride = trim((string) ($data['accounting_account_code'] ?? ''));
|
||||
$isColleagueLeg = ! empty($data['colleague_leg']) && $accountOverride !== '';
|
||||
|
||||
if ($customer && ($customer->moshtari_code === null)) {
|
||||
$this->createCustomerInZarin($customer->id);
|
||||
$customer = Customer::where('id', $data['customer_id'])->first();
|
||||
if (! $isColleagueLeg) {
|
||||
if ($customer && ($customer->moshtari_code === null)) {
|
||||
$this->createCustomerInZarin($customer->id);
|
||||
$customer = Customer::where('id', $data['customer_id'])->first();
|
||||
}
|
||||
|
||||
if (!$customer || empty($customer->moshtari_code)) {
|
||||
return false;
|
||||
}
|
||||
} elseif (! $customer) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!$customer || empty($customer->moshtari_code)) {
|
||||
return false;
|
||||
$accountCustomer = $customer;
|
||||
if ($isColleagueLeg) {
|
||||
$accountCustomer = clone $customer;
|
||||
$accountCustomer->moshtari_code = $accountOverride;
|
||||
}
|
||||
|
||||
$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);
|
||||
$orderId = is_object($data['order'] ?? null)
|
||||
? ($data['order']->id ?? null)
|
||||
: ($data['order']['id'] ?? ($existing->order_id ?? null));
|
||||
$title = $mapper->productTitle($product);
|
||||
$action = $isBuy ? 'خرید' : 'فروش';
|
||||
$sharh = trim($title !== '' ? "{$action} {$title}" : "{$action} {$mapper->kindLabel($mapper->resolveKind($product))}");
|
||||
$sharh = trim((string) ($data['sharh_override'] ?? ''));
|
||||
if ($sharh === '') {
|
||||
$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'];
|
||||
}
|
||||
if ($isColleagueLeg) {
|
||||
$uidSource = ($uidSource ?? '0') . 'C';
|
||||
}
|
||||
|
||||
$data['handler'] = 'storeTrade';
|
||||
if ($existing && empty($existing->data)) {
|
||||
@@ -1440,6 +1460,8 @@ class ZarinController extends Controller
|
||||
]);
|
||||
}
|
||||
|
||||
$uidKind = $isColleagueLeg ? '11' : '10';
|
||||
|
||||
try {
|
||||
if ($mapper->isCountDoc($product)) {
|
||||
$fi = (float) ($data['price'] ?? 0);
|
||||
@@ -1453,25 +1475,25 @@ class ZarinController extends Controller
|
||||
$result = $this->postZarinCountDoc(
|
||||
$zarin,
|
||||
$data,
|
||||
$customer,
|
||||
$accountCustomer,
|
||||
$product,
|
||||
$count,
|
||||
$isBuy ? -1 : 1,
|
||||
$fi,
|
||||
$sharh,
|
||||
$this->zarinUid('10', $uidSource ?? $zarin->id)
|
||||
$this->zarinUid($uidKind, $uidSource ?? $zarin->id)
|
||||
);
|
||||
} else {
|
||||
$result = $this->postZarinWeightDoc(
|
||||
$zarin,
|
||||
$data,
|
||||
$customer,
|
||||
$accountCustomer,
|
||||
$vazn,
|
||||
$isBuy ? 'f' : 'k',
|
||||
(float) ($data['price'] ?? 0),
|
||||
$sharh,
|
||||
$mapper->jensFelez($product) ?? 0,
|
||||
$this->zarinUid('10', $uidSource ?? $zarin->id)
|
||||
$this->zarinUid($uidKind, $uidSource ?? $zarin->id)
|
||||
);
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
|
||||
@@ -198,6 +198,25 @@ class PargarInboundController extends Controller
|
||||
}
|
||||
}
|
||||
|
||||
$colleagueLogs = PargarSyncLog::query()
|
||||
->where('type', PargarSyncService::TYPE_ORDER)
|
||||
->where('sync_status', PargarSyncService::STATUS_PENDING)
|
||||
->where('data', 'like', '%"colleague_leg":true%')
|
||||
->orderBy('id')
|
||||
->limit(100)
|
||||
->get();
|
||||
|
||||
foreach ($colleagueLogs as $log) {
|
||||
$order = Order::find((int) ($log->order_id ?? 0));
|
||||
if (! $order) {
|
||||
continue;
|
||||
}
|
||||
$payload = $this->orderExportPayload($order, $log);
|
||||
if ($payload !== null) {
|
||||
$documents->push($payload);
|
||||
}
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'state' => 'true',
|
||||
'msg' => '',
|
||||
@@ -583,12 +602,23 @@ class PargarInboundController extends Controller
|
||||
/**
|
||||
* @return array<string, mixed>|null
|
||||
*/
|
||||
private function orderExportPayload(Order $order): ?array
|
||||
private function orderExportPayload(Order $order, ?PargarSyncLog $forcedLog = null): ?array
|
||||
{
|
||||
$sync = $this->sync();
|
||||
$log = PargarSyncLog::where('order_id', $order->id)->latest('id')->first();
|
||||
$log = $forcedLog ?? PargarSyncLog::where('order_id', $order->id)
|
||||
->where(function ($q) {
|
||||
$q->whereNull('data')
|
||||
->orWhere('data', 'not like', '%"colleague_leg":true%');
|
||||
})
|
||||
->latest('id')
|
||||
->first();
|
||||
$payload = $log ? (json_decode((string) $log->data, true) ?: []) : [];
|
||||
|
||||
// اگر لاگ اجباری همکار نیست و لاگ مشتری نبود، سند مشتری نساز
|
||||
if ($forcedLog === null && $log && ! empty($payload['colleague_leg'])) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$paymentKind = (string) ($payload['payment_kind'] ?? PargarSyncService::PAYMENT_CASH);
|
||||
if ($paymentKind === PargarSyncService::PAYMENT_OTHER_PRODUCT) {
|
||||
return null;
|
||||
@@ -602,6 +632,8 @@ class PargarInboundController extends Controller
|
||||
$isCustomerBuy = (int) ($order->direction ?? 1) === 1;
|
||||
$customerDirection = (int) ($order->direction ?? 0);
|
||||
$fromCart = (bool) ($payload['from_cart'] ?? $this->isCartOrder($order));
|
||||
$isColleagueLeg = ! empty($payload['colleague_leg']);
|
||||
$colleagueAccountId = trim((string) ($payload['colleague_account_id'] ?? ''));
|
||||
|
||||
$product = $sync->resolveOrderProduct($order, $payload);
|
||||
$number = (float) ($payload['number'] ?? 0);
|
||||
@@ -609,6 +641,22 @@ class PargarInboundController extends Controller
|
||||
$totalPrice = (float) ($order->total_price ?? $order->payment_amount ?? 0);
|
||||
$unitPrice = (float) ($order->price ?? 0);
|
||||
|
||||
if ($isColleagueLeg) {
|
||||
if (isset($payload['direction'])) {
|
||||
$isCustomerBuy = (int) $payload['direction'] === 1;
|
||||
$customerDirection = (int) $payload['direction'];
|
||||
}
|
||||
if (isset($payload['total_price'])) {
|
||||
$totalPrice = (float) $payload['total_price'];
|
||||
}
|
||||
if (isset($payload['price'])) {
|
||||
$unitPrice = (float) $payload['price'];
|
||||
}
|
||||
if (isset($payload['amount'])) {
|
||||
$amount = abs((float) $payload['amount']);
|
||||
}
|
||||
}
|
||||
|
||||
$isCountBased = $product
|
||||
? self::isCountBasedProduct(self::productUnitPayload($product))
|
||||
: false;
|
||||
@@ -660,11 +708,19 @@ class PargarInboundController extends Controller
|
||||
$totalPrice = 0;
|
||||
}
|
||||
|
||||
$accountId = $isColleagueLeg && $colleagueAccountId !== ''
|
||||
? (int) $colleagueAccountId
|
||||
: (int) ($customer->pargar_account_id ?? 0);
|
||||
|
||||
$description = $isColleagueLeg
|
||||
? (((int) ($payload['direction'] ?? $customerDirection) === 1) ? 'خرید از اپلیکیشن همکار' : 'فروش از اپلیکیشن همکار')
|
||||
: self::accountingShopTradeDesc($customerDirection, $fromCart);
|
||||
|
||||
return [
|
||||
'OrderId' => (string) $order->id,
|
||||
'OrderId' => $isColleagueLeg ? ((string) $order->id).'-C' : (string) $order->id,
|
||||
'OrderType' => $isCustomerBuy ? -1 : 1,
|
||||
'PriceType' => 1,
|
||||
'CustomerAccountId' => (int) ($customer->pargar_account_id ?? 0),
|
||||
'CustomerAccountId' => $accountId,
|
||||
'ItemId' => $itemId,
|
||||
'ItemName' => $itemName,
|
||||
'OrderDate' => $order->pay_at
|
||||
@@ -675,7 +731,7 @@ class PargarInboundController extends Controller
|
||||
'Weight' => $weight,
|
||||
'Price' => $totalPrice,
|
||||
'Carat' => 750,
|
||||
'Description' => self::accountingShopTradeDesc($customerDirection, $fromCart),
|
||||
'Description' => $description,
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -50,9 +50,37 @@ class PublicStorageController extends Controller
|
||||
*/
|
||||
private function candidatePaths(string $path): array
|
||||
{
|
||||
return [
|
||||
storage_path('app/public/'.$path),
|
||||
public_path('storage/'.$path),
|
||||
];
|
||||
$relative = [$path, ...$this->alternateExtensionPaths($path)];
|
||||
$full = [];
|
||||
foreach ($relative as $rel) {
|
||||
$full[] = storage_path('app/public/'.$rel);
|
||||
$full[] = storage_path('app/public/storage/'.$rel);
|
||||
$full[] = public_path('storage/'.$rel);
|
||||
}
|
||||
|
||||
return $full;
|
||||
}
|
||||
|
||||
/**
|
||||
* آپلود ادمین WebP میسازد ولی const گاهی همان هش را با .png نگه میدارد.
|
||||
*
|
||||
* @return list<string>
|
||||
*/
|
||||
private function alternateExtensionPaths(string $path): array
|
||||
{
|
||||
if (! preg_match('/^(.*)\.([a-z0-9]+)$/i', $path, $matches)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$stem = $matches[1];
|
||||
$ext = strtolower($matches[2]);
|
||||
$out = [];
|
||||
foreach (['webp', 'png', 'jpg', 'jpeg', 'gif'] as $alt) {
|
||||
if ($alt !== $ext) {
|
||||
$out[] = $stem.'.'.$alt;
|
||||
}
|
||||
}
|
||||
|
||||
return $out;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -263,6 +263,15 @@ class SingleOrderController extends Controller
|
||||
report($e);
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($data['order'])) {
|
||||
try {
|
||||
app(\App\Services\ColleagueAccountingService::class)->recordCoveredTrade($data);
|
||||
} catch (\Throwable $e) {
|
||||
report($e);
|
||||
}
|
||||
}
|
||||
|
||||
$response['reward'] = $this->give_order_reward($data, $wallet, $wallet_interface);
|
||||
if ((int) $data['direction'] === 1 && $customer) {
|
||||
$referrer = Customer::where('referral_id', $customer->referral_to_id)->first();
|
||||
|
||||
Reference in New Issue
Block a user