Files
hub/app/Http/Controllers/Seller/Management/ArController.php
kelly 6d64d9527a feat: implement Management Suite core features
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
2025-12-07 00:34:53 -07:00

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.');
}
}