236 lines
8.8 KiB
PHP
236 lines
8.8 KiB
PHP
<?php
|
|
|
|
namespace Modules\AssetExchange\Services;
|
|
|
|
use App\Business;
|
|
use App\User;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Illuminate\Support\Facades\Schema;
|
|
use Modules\AssetExchange\Models\Commission;
|
|
use Modules\AssetExchange\Models\Deal;
|
|
use Modules\AssetExchange\Models\Listing;
|
|
use Modules\AssetExchange\Utils\AssetExchangeUtil;
|
|
|
|
class DealService
|
|
{
|
|
public const DEAL_TYPES = [
|
|
'company_purchase',
|
|
'brokerage',
|
|
'appraisal_only',
|
|
'renovation_contract',
|
|
];
|
|
|
|
public function __construct(
|
|
protected AssetExchangeUtil $aexUtil,
|
|
protected AssetExchangeErpBridgeService $erpBridge,
|
|
protected AssetExchangeCmmsBridgeService $cmmsBridge,
|
|
protected AssetExchangeCrmBridgeService $crmBridge,
|
|
protected PipelineService $pipelineService,
|
|
protected ListingService $listingService
|
|
) {
|
|
}
|
|
|
|
public function createDeal(Business $business, Listing $listing, string $dealType, array $data = [], ?User $user = null): Deal
|
|
{
|
|
if (! in_array($dealType, self::DEAL_TYPES, true)) {
|
|
throw new \InvalidArgumentException('نوع معامله نامعتبر است: '.$dealType);
|
|
}
|
|
|
|
$settings = $this->aexUtil->getAexSettings($business->id);
|
|
|
|
if (($settings['require_appraisal_before_deal'] ?? true) && ! in_array($listing->status, ['appraised', 'in_deal'], true)) {
|
|
throw new \RuntimeException('قبل از ایجاد معامله، کارشناسی عرضه باید تکمیل شود.');
|
|
}
|
|
|
|
$appraisal = null;
|
|
if (! empty($data['appraisal_id'])) {
|
|
$appraisal = \Modules\AssetExchange\Models\Appraisal::where('business_id', $business->id)
|
|
->where('listing_id', $listing->id)
|
|
->find($data['appraisal_id']);
|
|
} else {
|
|
$appraisal = $listing->appraisals()->where('status', 'approved')->latest('id')->first();
|
|
}
|
|
|
|
return DB::transaction(function () use ($business, $listing, $dealType, $data, $user, $settings, $appraisal) {
|
|
$agreedPrice = (int) ($data['agreed_price']
|
|
?? $appraisal?->recommended_buy_price
|
|
?? $listing->asking_price);
|
|
|
|
$commissionPercent = (float) ($data['commission_percent']
|
|
?? $settings['default_commission_percent']
|
|
?? 0);
|
|
|
|
$deal = Deal::create([
|
|
'business_id' => $business->id,
|
|
'listing_id' => $listing->id,
|
|
'appraisal_id' => $appraisal?->id,
|
|
'deal_code' => $data['deal_code'] ?? $this->aexUtil->generateDealCode($business->id),
|
|
'deal_type' => $dealType,
|
|
'buyer_contact_id' => $data['buyer_contact_id'] ?? null,
|
|
'status' => 'negotiating',
|
|
'agreed_price' => $agreedPrice,
|
|
'commission_percent' => $commissionPercent,
|
|
'commission_amount' => (int) round($agreedPrice * ($commissionPercent / 100)),
|
|
'notes' => $data['notes'] ?? null,
|
|
'created_by' => $user?->id,
|
|
]);
|
|
|
|
$listing->update(['status' => 'in_deal']);
|
|
|
|
$stage = match ($dealType) {
|
|
'company_purchase' => 'company_buy',
|
|
'brokerage' => 'broker_sale',
|
|
'renovation_contract' => 'renovation_contract',
|
|
default => 'deal_negotiation',
|
|
};
|
|
|
|
$this->pipelineService->advanceStage($business, $listing, 'deal_negotiation', $deal->id, Deal::class, $deal->id, $user);
|
|
$this->pipelineService->advanceStage($business, $listing, $stage, $deal->id, Deal::class, $deal->id, $user);
|
|
$this->pipelineService->logEvent(
|
|
$business,
|
|
Deal::class,
|
|
$deal->id,
|
|
'deal_created',
|
|
'معامله جدید ایجاد شد: '.$this->aexUtil->dealTypeLabel($dealType),
|
|
$user
|
|
);
|
|
|
|
$this->bootstrapDealRecords($business, $listing, $deal, $user);
|
|
|
|
return $deal->fresh(['listing', 'appraisal']);
|
|
});
|
|
}
|
|
|
|
public function updateStatus(Business $business, Deal $deal, string $status, ?User $user = null): Deal
|
|
{
|
|
$allowed = ['negotiating', 'quoted', 'contracted', 'in_progress', 'completed', 'cancelled'];
|
|
if (! in_array($status, $allowed, true)) {
|
|
throw new \InvalidArgumentException('وضعیت معامله نامعتبر است.');
|
|
}
|
|
|
|
$deal->update(['status' => $status]);
|
|
|
|
$this->pipelineService->logEvent(
|
|
$business,
|
|
Deal::class,
|
|
$deal->id,
|
|
'deal_status_changed',
|
|
'وضعیت معامله به «'.$this->aexUtil->statusLabel($status).'» تغییر کرد',
|
|
$user,
|
|
['status' => $status]
|
|
);
|
|
|
|
return $deal->fresh();
|
|
}
|
|
|
|
public function completeDeal(Business $business, Deal $deal, ?User $user = null): Deal
|
|
{
|
|
return DB::transaction(function () use ($business, $deal, $user) {
|
|
$deal->loadMissing('listing', 'appraisal');
|
|
$listing = $deal->listing;
|
|
|
|
$deal->update(['status' => 'completed']);
|
|
|
|
if ($listing) {
|
|
$listing->update(['status' => 'sold']);
|
|
$this->pipelineService->advanceStage($business, $listing, 'sold', $deal->id, Deal::class, $deal->id, $user);
|
|
}
|
|
|
|
if ($deal->deal_type === 'brokerage' && $deal->commission_amount > 0) {
|
|
$this->ensureBrokerCommission($deal, $user);
|
|
$this->crmBridge->syncCommissions($business, $deal);
|
|
}
|
|
|
|
if ($deal->deal_type === 'company_purchase' && ($this->aexUtil->getAexSettings($business->id)['auto_create_cmms_on_acquire'] ?? true)) {
|
|
try {
|
|
$this->cmmsBridge->createEquipmentFromListing($business, $listing, $user);
|
|
} catch (\Throwable) {
|
|
}
|
|
}
|
|
|
|
if ($deal->deal_type === 'renovation_contract') {
|
|
try {
|
|
$this->cmmsBridge->startRebuildFromDeal($business, $deal, $user);
|
|
if ($listing) {
|
|
$this->pipelineService->advanceStage($business, $listing, 'in_renovation', $deal->id, Deal::class, $deal->id, $user);
|
|
}
|
|
} catch (\Throwable) {
|
|
}
|
|
}
|
|
|
|
$this->pipelineService->logEvent(
|
|
$business,
|
|
Deal::class,
|
|
$deal->id,
|
|
'deal_completed',
|
|
'معامله تکمیل شد',
|
|
$user
|
|
);
|
|
|
|
return $deal->fresh(['listing', 'commissions']);
|
|
});
|
|
}
|
|
|
|
public function linkTransaction(Deal $deal, string $side, int $transactionId): Deal
|
|
{
|
|
if (! Schema::hasTable('transactions')) {
|
|
throw new \RuntimeException('جدول تراکنشها موجود نیست.');
|
|
}
|
|
|
|
$field = match ($side) {
|
|
'purchase' => 'transaction_purchase_id',
|
|
'sell' => 'transaction_sell_id',
|
|
default => throw new \InvalidArgumentException('نوع تراکنش باید purchase یا sell باشد.'),
|
|
};
|
|
|
|
$deal->update([$field => $transactionId]);
|
|
|
|
return $deal->fresh();
|
|
}
|
|
|
|
protected function bootstrapDealRecords(Business $business, Listing $listing, Deal $deal, ?User $user): void
|
|
{
|
|
try {
|
|
match ($deal->deal_type) {
|
|
'company_purchase' => $this->erpBridge->createPurchaseOrder($business, $listing, $deal, null, $user),
|
|
'brokerage' => $this->erpBridge->createQuotation($business, $listing, $deal, null, $user),
|
|
'appraisal_only' => $this->erpBridge->createSellInvoice(
|
|
$business,
|
|
$listing,
|
|
$deal->fresh(['appraisal']),
|
|
null,
|
|
$user
|
|
),
|
|
'renovation_contract' => $this->erpBridge->createQuotation($business, $listing, $deal, null, $user),
|
|
default => null,
|
|
};
|
|
} catch (\Throwable) {
|
|
}
|
|
}
|
|
|
|
protected function ensureBrokerCommission(Deal $deal, ?User $user): void
|
|
{
|
|
if (! Schema::hasTable('aex_commissions')) {
|
|
return;
|
|
}
|
|
|
|
$brokerId = $deal->listing?->assigned_broker_user_id ?: $user?->id;
|
|
if (! $brokerId) {
|
|
return;
|
|
}
|
|
|
|
Commission::updateOrCreate(
|
|
[
|
|
'deal_id' => $deal->id,
|
|
'user_id' => $brokerId,
|
|
'role' => 'broker',
|
|
],
|
|
[
|
|
'business_id' => $deal->business_id,
|
|
'amount' => $deal->commission_amount,
|
|
'status' => 'pending',
|
|
]
|
|
);
|
|
}
|
|
}
|