Bank Account Management: - BankAccountsController with full CRUD operations - BankAccountService for account management logic - Bank account views (index, create, edit) - GL account linking for cash accounts Bank Transfers: - BankTransfersController with approval workflow - BankTransfer model with status management - Inter-business transfer support - Transfer views (index, create, show) Plaid Integration Infrastructure: - PlaidItem, PlaidAccount, PlaidTransaction models - PlaidIntegrationService with 10+ methods (stubbed API) - BankReconciliationController with match/learn flows - Bank match rules for auto-categorization Journal Entry Automation: - JournalEntryService for automatic JE creation - Bill approval creates expense entries - Payment completion creates cash entries - Inter-company Due To/Due From entries AR Enhancements: - ArService with getArSummary and getTopArAccounts - Finance dashboard AR section with drill-down stats - Credit hold and at-risk tracking - Top AR accounts table with division column UI/Navigation: - Updated SuiteMenuResolver with new menu items - Removed Usage & Billing from sidebar (moved to Owner) - Brand Manager Suite menu items added - Vendors page shows divisions using each vendor Models and Migrations: - BankAccount, BankTransfer, BankMatchRule models - PlaidItem, PlaidAccount, PlaidTransaction models - Credit hold fields on ArCustomer - journal_entry_id on ap_bills and ap_payments
192 lines
6.1 KiB
PHP
192 lines
6.1 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Http\Controllers\Seller\Management;
|
|
|
|
use App\Http\Controllers\Controller;
|
|
use App\Models\Accounting\ArCustomer;
|
|
use App\Models\Business;
|
|
use App\Services\Accounting\ArAnalyticsService;
|
|
use App\Services\Accounting\ArService;
|
|
use App\Services\Accounting\CustomerFinancialService;
|
|
use App\Support\ManagementDivisionFilter;
|
|
use Illuminate\Http\Request;
|
|
|
|
class ArController extends Controller
|
|
{
|
|
use ManagementDivisionFilter;
|
|
|
|
public function __construct(
|
|
protected ArAnalyticsService $analyticsService,
|
|
protected ArService $arService,
|
|
protected CustomerFinancialService $customerService
|
|
) {}
|
|
|
|
/**
|
|
* AR Overview dashboard.
|
|
*/
|
|
public function index(Request $request, Business $business)
|
|
{
|
|
$filterData = $this->getDivisionFilterData($business, $request);
|
|
|
|
$metrics = $this->analyticsService->getARMetrics($business, $filterData['business_ids']);
|
|
$aging = $this->analyticsService->getARAging($business, $filterData['business_ids']);
|
|
$topCustomers = $this->analyticsService->getARBreakdownByCustomer($business, $filterData['business_ids'], 5);
|
|
|
|
return view('seller.management.ar.index', $this->withDivisionFilter([
|
|
'business' => $business,
|
|
'metrics' => $metrics,
|
|
'aging' => $aging,
|
|
'topCustomers' => $topCustomers,
|
|
], $filterData));
|
|
}
|
|
|
|
/**
|
|
* AR Aging detail page.
|
|
*/
|
|
public function aging(Request $request, Business $business)
|
|
{
|
|
$filterData = $this->getDivisionFilterData($business, $request);
|
|
|
|
$aging = $this->analyticsService->getARAging($business, $filterData['business_ids']);
|
|
$byDivision = $this->analyticsService->getARBreakdownByDivision($business, $filterData['business_ids']);
|
|
$byCustomer = $this->analyticsService->getARBreakdownByCustomer($business, $filterData['business_ids'], 10);
|
|
|
|
// Check for bucket filter from drill-down
|
|
$bucket = $request->get('bucket');
|
|
|
|
return view('seller.management.ar.aging', $this->withDivisionFilter([
|
|
'business' => $business,
|
|
'aging' => $aging,
|
|
'byDivision' => $byDivision,
|
|
'byCustomer' => $byCustomer,
|
|
'activeBucket' => $bucket,
|
|
], $filterData));
|
|
}
|
|
|
|
/**
|
|
* AR Accounts list page.
|
|
*/
|
|
public function accounts(Request $request, Business $business)
|
|
{
|
|
$filterData = $this->getDivisionFilterData($business, $request);
|
|
|
|
$filters = [
|
|
'on_hold' => $request->boolean('on_hold'),
|
|
'at_risk' => $request->boolean('at_risk'),
|
|
'search' => $request->get('search'),
|
|
];
|
|
|
|
$accounts = $this->arService->getAccountsWithBalances(
|
|
$business,
|
|
$filterData['business_ids'],
|
|
$filters
|
|
);
|
|
|
|
$metrics = $this->analyticsService->getARMetrics($business, $filterData['business_ids']);
|
|
|
|
return view('seller.management.ar.accounts', $this->withDivisionFilter([
|
|
'business' => $business,
|
|
'accounts' => $accounts,
|
|
'metrics' => $metrics,
|
|
'filters' => $filters,
|
|
], $filterData));
|
|
}
|
|
|
|
/**
|
|
* Single account detail page.
|
|
*/
|
|
public function showAccount(Request $request, Business $business, ArCustomer $customer)
|
|
{
|
|
// Verify customer belongs to this business or a child
|
|
$isParent = $this->arService->isParentCompany($business);
|
|
$allowedBusinessIds = $isParent
|
|
? $this->arService->getBusinessIdsWithChildren($business)
|
|
: [$business->id];
|
|
|
|
if (! in_array($customer->business_id, $allowedBusinessIds)) {
|
|
abort(404);
|
|
}
|
|
|
|
$summary = $this->customerService->getFinancialSummary($customer, $business, $isParent);
|
|
$invoices = $this->customerService->getInvoices($customer, $business, $isParent);
|
|
$payments = $this->customerService->getPayments($customer, $business, $isParent);
|
|
$activities = $this->customerService->getRecentActivity($customer, $business);
|
|
|
|
return view('seller.management.ar.account-detail', [
|
|
'business' => $business,
|
|
'customer' => $customer,
|
|
'summary' => $summary,
|
|
'invoices' => $invoices,
|
|
'payments' => $payments,
|
|
'activities' => $activities,
|
|
'isParent' => $isParent,
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Update credit limit (Management only).
|
|
*/
|
|
public function updateCreditLimit(Request $request, Business $business, ArCustomer $customer)
|
|
{
|
|
$request->validate([
|
|
'credit_limit' => 'required|numeric|min:0',
|
|
]);
|
|
|
|
$this->arService->updateCreditLimit(
|
|
$customer,
|
|
(float) $request->input('credit_limit'),
|
|
auth()->id()
|
|
);
|
|
|
|
return back()->with('success', 'Credit limit updated successfully.');
|
|
}
|
|
|
|
/**
|
|
* Update payment terms (Management only).
|
|
*/
|
|
public function updateTerms(Request $request, Business $business, ArCustomer $customer)
|
|
{
|
|
$request->validate([
|
|
'payment_terms' => 'required|string',
|
|
]);
|
|
|
|
$this->arService->updatePaymentTerms(
|
|
$customer,
|
|
$request->input('payment_terms'),
|
|
auth()->id()
|
|
);
|
|
|
|
return back()->with('success', 'Payment terms updated successfully.');
|
|
}
|
|
|
|
/**
|
|
* Place credit hold (Management only).
|
|
*/
|
|
public function placeHold(Request $request, Business $business, ArCustomer $customer)
|
|
{
|
|
$request->validate([
|
|
'reason' => 'required|string|max:500',
|
|
]);
|
|
|
|
$this->arService->placeCreditHold(
|
|
$customer,
|
|
$request->input('reason'),
|
|
auth()->id()
|
|
);
|
|
|
|
return back()->with('success', 'Credit hold placed successfully.');
|
|
}
|
|
|
|
/**
|
|
* Remove credit hold (Management only).
|
|
*/
|
|
public function removeHold(Request $request, Business $business, ArCustomer $customer)
|
|
{
|
|
$this->arService->removeCreditHold($customer, auth()->id());
|
|
|
|
return back()->with('success', 'Credit hold removed successfully.');
|
|
}
|
|
}
|