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
194 lines
7.2 KiB
PHP
194 lines
7.2 KiB
PHP
<?php
|
|
|
|
namespace App\Http\Controllers\Seller\Management;
|
|
|
|
use App\Http\Controllers\Controller;
|
|
use App\Models\Business;
|
|
use App\Support\ManagementDivisionFilter;
|
|
use Carbon\Carbon;
|
|
use Illuminate\Http\Request;
|
|
use Illuminate\Support\Facades\DB;
|
|
|
|
class UsageBillingController extends Controller
|
|
{
|
|
use ManagementDivisionFilter;
|
|
|
|
public function index(Request $request, Business $business)
|
|
{
|
|
$this->requireManagementSuite($business);
|
|
|
|
$divisions = $this->getChildDivisionsIfAny($business);
|
|
$selectedDivision = $this->getSelectedDivision($request, $business);
|
|
$includeChildren = $this->shouldIncludeChildren($request);
|
|
|
|
$businessIds = $this->getBusinessIdsForScope($business, $selectedDivision, $includeChildren);
|
|
|
|
// Collect usage data
|
|
$usage = $this->collectUsageData($business, $businessIds);
|
|
|
|
return view('seller.management.usage-billing.index', [
|
|
'business' => $business,
|
|
'divisions' => $divisions,
|
|
'selectedDivision' => $selectedDivision,
|
|
'includeChildren' => $includeChildren,
|
|
'usage' => $usage,
|
|
]);
|
|
}
|
|
|
|
protected function collectUsageData(Business $parentBusiness, array $businessIds): array
|
|
{
|
|
$startOfMonth = Carbon::now()->startOfMonth();
|
|
$endOfMonth = Carbon::now()->endOfMonth();
|
|
|
|
// Get suite limits from config
|
|
$defaults = config('suites.defaults.sales_suite', []);
|
|
|
|
// Count active brands
|
|
$brandCount = DB::table('brands')
|
|
->whereIn('business_id', $businessIds)
|
|
->where('is_active', true)
|
|
->count();
|
|
|
|
// Count active products (SKUs)
|
|
$skuCount = DB::table('products')
|
|
->join('brands', 'products.brand_id', '=', 'brands.id')
|
|
->whereIn('brands.business_id', $businessIds)
|
|
->where('products.is_active', true)
|
|
->count();
|
|
|
|
// Count messages sent this month
|
|
$messageCount = DB::table('messages')
|
|
->whereIn('business_id', $businessIds)
|
|
->whereBetween('created_at', [$startOfMonth, $endOfMonth])
|
|
->count();
|
|
|
|
// Count menu sends this month
|
|
$menuSendCount = DB::table('menu_sends')
|
|
->whereIn('business_id', $businessIds)
|
|
->whereBetween('created_at', [$startOfMonth, $endOfMonth])
|
|
->count();
|
|
|
|
// Count CRM contacts
|
|
$contactCount = DB::table('contacts')
|
|
->whereIn('business_id', $businessIds)
|
|
->count();
|
|
|
|
// Calculate limits based on number of brands
|
|
$brandLimit = $parentBusiness->brand_limit ?? $defaults['brand_limit'] ?? 1;
|
|
$skuLimitPerBrand = $defaults['sku_limit_per_brand'] ?? 15;
|
|
$messageLimitPerBrand = $defaults['message_limit_per_brand'] ?? 500;
|
|
$menuLimitPerBrand = $defaults['menu_limit_per_brand'] ?? 100;
|
|
$contactLimitPerBrand = $defaults['contact_limit_per_brand'] ?? 1000;
|
|
|
|
$totalSkuLimit = $brandCount * $skuLimitPerBrand;
|
|
$totalMessageLimit = $brandCount * $messageLimitPerBrand;
|
|
$totalMenuLimit = $brandCount * $menuLimitPerBrand;
|
|
$totalContactLimit = $brandCount * $contactLimitPerBrand;
|
|
|
|
// Is enterprise plan?
|
|
$isEnterprise = $parentBusiness->is_enterprise_plan ?? false;
|
|
|
|
// Get suites enabled
|
|
$enabledSuites = $this->getEnabledSuites($parentBusiness);
|
|
|
|
// Usage by division
|
|
$usageByDivision = [];
|
|
if (count($businessIds) > 1) {
|
|
$usageByDivision = DB::table('businesses')
|
|
->whereIn('businesses.id', $businessIds)
|
|
->leftJoin('brands', 'brands.business_id', '=', 'businesses.id')
|
|
->leftJoin('products', 'products.brand_id', '=', 'brands.id')
|
|
->select(
|
|
'businesses.id',
|
|
'businesses.name',
|
|
DB::raw('COUNT(DISTINCT brands.id) as brand_count'),
|
|
DB::raw('COUNT(DISTINCT products.id) as sku_count')
|
|
)
|
|
->groupBy('businesses.id', 'businesses.name')
|
|
->get();
|
|
}
|
|
|
|
return [
|
|
'brands' => [
|
|
'current' => $brandCount,
|
|
'limit' => $isEnterprise ? null : $brandLimit,
|
|
'percentage' => $brandLimit > 0 ? min(100, ($brandCount / $brandLimit) * 100) : 0,
|
|
],
|
|
'skus' => [
|
|
'current' => $skuCount,
|
|
'limit' => $isEnterprise ? null : $totalSkuLimit,
|
|
'percentage' => $totalSkuLimit > 0 ? min(100, ($skuCount / $totalSkuLimit) * 100) : 0,
|
|
],
|
|
'messages' => [
|
|
'current' => $messageCount,
|
|
'limit' => $isEnterprise ? null : $totalMessageLimit,
|
|
'percentage' => $totalMessageLimit > 0 ? min(100, ($messageCount / $totalMessageLimit) * 100) : 0,
|
|
],
|
|
'menu_sends' => [
|
|
'current' => $menuSendCount,
|
|
'limit' => $isEnterprise ? null : $totalMenuLimit,
|
|
'percentage' => $totalMenuLimit > 0 ? min(100, ($menuSendCount / $totalMenuLimit) * 100) : 0,
|
|
],
|
|
'contacts' => [
|
|
'current' => $contactCount,
|
|
'limit' => $isEnterprise ? null : $totalContactLimit,
|
|
'percentage' => $totalContactLimit > 0 ? min(100, ($contactCount / $totalContactLimit) * 100) : 0,
|
|
],
|
|
'is_enterprise' => $isEnterprise,
|
|
'enabled_suites' => $enabledSuites,
|
|
'usage_by_division' => $usageByDivision,
|
|
'billing_period' => [
|
|
'start' => $startOfMonth->format('M j, Y'),
|
|
'end' => $endOfMonth->format('M j, Y'),
|
|
],
|
|
];
|
|
}
|
|
|
|
protected function getEnabledSuites(Business $business): array
|
|
{
|
|
$suites = [];
|
|
|
|
if ($business->hasSalesSuite()) {
|
|
$suites[] = ['name' => 'Sales Suite', 'key' => 'sales'];
|
|
}
|
|
if ($business->hasProcessingSuite()) {
|
|
$suites[] = ['name' => 'Processing Suite', 'key' => 'processing'];
|
|
}
|
|
if ($business->hasManufacturingSuite()) {
|
|
$suites[] = ['name' => 'Manufacturing Suite', 'key' => 'manufacturing'];
|
|
}
|
|
if ($business->hasDeliverySuite()) {
|
|
$suites[] = ['name' => 'Delivery Suite', 'key' => 'delivery'];
|
|
}
|
|
if ($business->hasManagementSuite()) {
|
|
$suites[] = ['name' => 'Management Suite', 'key' => 'management'];
|
|
}
|
|
if ($business->hasDispensarySuite()) {
|
|
$suites[] = ['name' => 'Dispensary Suite', 'key' => 'dispensary'];
|
|
}
|
|
|
|
return $suites;
|
|
}
|
|
|
|
protected function getBusinessIdsForScope(Business $business, ?Business $selectedDivision, bool $includeChildren): array
|
|
{
|
|
if ($selectedDivision) {
|
|
if ($includeChildren) {
|
|
return $selectedDivision->divisions()->pluck('id')
|
|
->prepend($selectedDivision->id)
|
|
->toArray();
|
|
}
|
|
|
|
return [$selectedDivision->id];
|
|
}
|
|
|
|
if ($includeChildren && $business->hasChildBusinesses()) {
|
|
return $business->divisions()->pluck('id')
|
|
->prepend($business->id)
|
|
->toArray();
|
|
}
|
|
|
|
return [$business->id];
|
|
}
|
|
}
|