inspiren-sem-tool/app/Http/Controllers/GoogleAdsController.php
brian-inspiren 221d3f8173
Some checks failed
linter / quality (push) Has been cancelled
tests / ci (push) Has been cancelled
feat: sem codebase
2026-05-21 11:28:03 +08:00

491 lines
20 KiB
PHP

<?php
namespace App\Http\Controllers;
use App\Models\Client;
use App\Models\ClientProjectActivities;
use App\Models\ClientUserAssignation;
use App\Models\ClientInvoiceAdjustment;
use App\Models\ClientCustomer;
use App\Models\GoogleCampaignMetric;
use App\Models\User;
use App\Models\ClientInvoice;
use App\Services\GoogleAdsService;
use App\Services\UserHierarchyService;
use Carbon\Carbon;
use Inertia\Inertia;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Artisan;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Log as FacadesLog;
use Rap2hpoutre\FastExcel\FastExcel;
class GoogleAdsController extends Controller
{
protected $adsService;
private const GOOGLE_COMPANY_SYNC_LOCK = 'google-ads:get-company-details:running';
public function __construct(
GoogleAdsService $adsService,
private UserHierarchyService $hierarchyService,
)
{
$this->adsService = $adsService;
}
// List all accounts under MCC
public function accounts()
{
// $accounts = collect($this->adsService->listAccounts())
// ->map(function (array $account) {
// $account['customer_id'] = (string) ($account['customer_id'] ?? $account['id']);
// return $account;
// });
// $customerMap = $accounts->keyBy('customer_id');
$localClients = $this->hierarchyService
->scopeClientsVisibleTo(Client::query(), Auth::user())
->with('assignations.user', 'customers', 'invoices', 'invoiceAdjustments')
->get();
$customerMap = $localClients->map(function ($data) {
$assignedPerson = $data->assignations->firstWhere('role', ClientUserAssignation::ROLE_ASSIGNED_PERSON);
$salesPerson = $data->assignations->firstWhere('role', ClientUserAssignation::ROLE_SALES_PERSON);
$data['industry'] = $data->industry;
$data['sql_acc_code'] = implode(',', $data->customers->pluck('sql_acc_code')->toArray());
$data['assigned_person'] = $assignedPerson?->user?->name;
$data['sales_person'] = $salesPerson?->user?->name;
$data['latest_remaining_amount'] = $data->latestRemainingAmount(function (ClientInvoice $invoice) use ($data) {
if (empty($invoice->start_date) || empty($invoice->end_date)) {
return 0;
}
return GoogleCampaignMetric::query()
->join('google_campaigns', 'google_campaign_metrics.google_campaign_id', '=', 'google_campaigns.id')
->where('google_campaigns.client_id', $data->id)
->whereNull('google_campaigns.deleted_at')
->whereNull('google_campaign_metrics.deleted_at')
->whereBetween('google_campaign_metrics.date', [
$invoice->start_date?->toDateString(),
$invoice->end_date?->toDateString(),
])
->sum('google_campaign_metrics.actual_spend');
});
return $data;
});
return Inertia::render('campaigns/index', [
'clients' => $customerMap->values()->all(),
'googleCompanySyncRunning' => Cache::has(self::GOOGLE_COMPANY_SYNC_LOCK),
]);
}
public function syncGoogleCompanyDetails(): RedirectResponse
{
$started = Cache::add(self::GOOGLE_COMPANY_SYNC_LOCK, [
'user_id' => Auth::id(),
'started_at' => now()->toDateTimeString(),
], now()->addHour());
if (! $started) {
return redirect()
->back()
->with('message-warning', 'Google account sync is already running. Please wait for it to finish.');
}
try {
$exitCode = Artisan::call('google-ads:get-company-details');
if ($exitCode !== 0) {
return redirect()
->back()
->with('message-error', 'Google account sync failed. Please check the logs.');
}
return redirect()
->back()
->with('message-info', 'Google account records have been synced.');
} finally {
Cache::forget(self::GOOGLE_COMPANY_SYNC_LOCK);
}
}
public function show($id)
{
// $account = $this->adsService->getAccountDetails($id);
$client = Client::where('customer_id', $id)->firstOrFail();
abort_unless($this->hierarchyService->canViewClient(Auth::user(), $client), 403);
$account = [
'id' => $client->customer_id,
'name' => $client->name,
'status' => $client->status,
'time_zone' => $client->time_zone,
];
[$localClient, $assignments, $users, $invoices, $activityList, $pendingActivities, $completedActivities, $lifeTimeSpending, $clientAdjustments] =
$this->hydrateClient($account);
$account = array_merge($account, [
'industry' => $localClient->industry,
'sql_acc_code' => $localClient->sql_acc_code,
'activities_list' => $activityList,
]);
return Inertia::render('campaigns/show', [
'id' => $id,
'client' => $account,
'invoices' => $invoices,
'localClientId' => $localClient->id,
'clientAssignmentRoles' => ClientUserAssignation::roles(),
'clientAssignments' => $assignments,
'assignmentUsers' => $users,
'clientInvoices' => $invoices,
'clientAdjustments' => $clientAdjustments,
'pendingActivities' => $pendingActivities,
'completedActivities' => $completedActivities,
'lifeTimeSpending' => number_format($lifeTimeSpending, 2, '.', ','),
]);
}
public function edit($id)
{
$account = $this->adsService->getAccountDetails($id);
[$localClient, $assignments, $users, $invoices] = $this->hydrateClient($account);
abort_unless($this->hierarchyService->canViewClient(Auth::user(), $localClient), 403);
$account = array_merge($account, [
'name' => $localClient->name,
'customer_id' => $localClient->customer_id,
'status' => $localClient->status,
'time_zone' => $localClient->time_zone,
'industry' => $localClient->industry,
'sql_acc_code' => $localClient->sql_acc_code,
]);
return Inertia::render('campaigns/account/edit', [
'id' => $id,
'client' => $account,
'clientAssignmentRoles' => ClientUserAssignation::roles(),
'clientAssignments' => $assignments,
'assignmentUsers' => $users,
'clientInvoices' => $invoices,
]);
}
public function updateAccount(Request $request, $id)
{
[$localClient] = $this->hydrateClient($this->adsService->getAccountDetails($id));
abort_unless($this->hierarchyService->canViewClient(Auth::user(), $localClient), 403);
$validated = $request->validate([
'name' => ['required', 'string'],
'customer_id' => ['required', 'string', 'unique:clients,customer_id,'.$localClient->id],
'industry' => ['nullable', 'string'],
'sql_acc_code' => ['nullable', 'string'],
'assigned_person' => ['nullable', 'integer', 'exists:users,id'],
'sales_person' => ['nullable', 'integer', 'exists:users,id'],
]);
$localClient->update([
'name' => $validated['name'],
'customer_id' => $validated['customer_id'],
'industry' => $validated['industry'] ?? null,
'sql_acc_code' => $validated['sql_acc_code'] ?? null,
]);
$localClient->customers()->delete();
if (! empty($validated['sql_acc_code'])) {
collect(explode(',', $validated['sql_acc_code']))
->map(fn (string $sqlAccCode) => trim($sqlAccCode))
->filter()
->unique()
->each(function (string $sqlAccCode) use ($localClient) {
ClientCustomer::create([
'client_id' => $localClient->id,
'sql_acc_code' => $sqlAccCode,
]);
});
}
$assignmentValues = [
ClientUserAssignation::ROLE_ASSIGNED_PERSON => $validated['assigned_person'] ?? null,
ClientUserAssignation::ROLE_SALES_PERSON => $validated['sales_person'] ?? null,
];
foreach ($assignmentValues as $role => $userId) {
if ($userId === null) {
$localClient->assignations()->where('role', $role)->delete();
continue;
}
$localClient->assignations()->updateOrCreate(
['role' => $role],
['user_id' => $userId]
);
}
return redirect()
->route('google-ads.accounts.edit', ['id' => $localClient->customer_id])
->with('message-info', 'Account details updated.');
}
public function campaigns($id)
{
$campaigns = $this->adsService->listCampaigns($id);
return response()->json($campaigns);
}
public function listCampaignsMetrics($id, $startDate, $endDate)
{
$campaigns = $this->adsService->listCampaignsMetrics($id, $startDate, $endDate);
return response()->json($campaigns);
}
private function hydrateClient(array $account): array
{
$localClient = Client::updateOrCreate(
['customer_id' => $account['id']],
[
'name' => $account['name'],
'status' => $account['status'],
'time_zone' => $account['time_zone'],
]
);
$localClient->load(['assignations.user', 'invoices']);
$assignments = $localClient->assignations
->mapWithKeys(function (ClientUserAssignation $assignation) {
return [$assignation->role => $assignation->user_id];
})
->toArray();
$users = User::orderBy('name')
->get(['id', 'name', 'email'])
->map(function (User $user) {
return [
'value' => (string) $user->id,
'label' => trim($user->name.' ('.$user->email.')'),
];
})
->toArray();
$clientAdjustments = ClientInvoiceAdjustment::query()
->where('client_id', $localClient->id)
->orderByDesc('created_at')
->get()
->map(function (ClientInvoiceAdjustment $adjustment) {
return [
'id' => $adjustment->id,
'client_id' => $adjustment->client_id,
'entry_type' => $adjustment->entry_type,
'amount' => $adjustment->amount,
'remark' => $adjustment->remark,
'created_at' => $adjustment->created_at?->toDateTimeString(),
];
})
->values()
->all();
$invoices = $localClient->invoices
->map(function ($invoice) use ($account) {
$campaigns = $this->adsService->listCampaigns($account['id']);
$totalInvoiceSpend = 0;
FacadesLog::info('Hydrated client data', [
'campaigns' => $campaigns,
]);
foreach ($campaigns as $campaign) {
FacadesLog::info('Hydrated client data', [
'campaigns' => $campaign['id'],
]);
if (empty($invoice->start_date) || empty($invoice->end_date)) {
continue;
} else {
$metrics = $this->adsService->listCampaignsMetricsById(
$account['id'],
$campaign['id'],
$invoice->start_date?->toDateString() ?? null,
$invoice->end_date?->toDateString() ?? null
);
FacadesLog::info('Hydrated client data', [
'metrics' => $metrics,
]);
$totalSpend = array_sum(array_column($metrics, 'actual_spend'));
$totalInvoiceSpend += $totalSpend;
}
}
return [
'id' => $invoice->id,
'client_id' => $invoice->client_id,
'invoice_no' => $invoice->invoice_no,
'linked_invoice_id' => $invoice->linked_invoice_id,
'is_credit_card' => $invoice->is_credit_card,
'is_paid' => $invoice->is_paid,
'start_date' => $invoice->start_date?->toDateString(),
'end_date' => $invoice->end_date?->toDateString(),
'payment_no' => $invoice->payment_no,
'amount' => $invoice->amount,
'total_spend' => number_format($totalInvoiceSpend, 2, '.', ''),
'management_fee' => $invoice->management_fee,
'media_fee' => $invoice->media_fee,
'tax_percent' => $invoice->tax_percent,
'nett_amount' => $invoice->nett_amount,
'total_spending' => $invoice->total_spending,
];
})
->toArray();
// dd($invoices);
$campaigns = $this->adsService->listCampaigns($localClient->customer_id);
$lifeTimeSpend = 0;
if (! empty($campaigns)) {
foreach ($campaigns as $campaign) {
$metrics = $this->adsService->listCampaignsMetricsById(
$localClient->customer_id,
$campaign['id'],
'1970-01-01', // Start date far in the past to capture all historical data
now()->toDateString() // End date as today`
);
$totalSpend = array_sum(array_column($metrics, 'actual_spend'));
$lifeTimeSpend += number_format($totalSpend, 2, '.', '');
}
}
$activities = $localClient->activitiesList()
->orderByDesc('created_at')
->get()
->map(function (ClientProjectActivities $activity) {
return [
'id' => $activity->id,
'activity_no' => $activity->activity_no,
'activity_type' => $activity->activity_type,
'task_description' => $activity->task_description,
'estimated_completed_at' => data_get($activity, 'estimated_completed_at'),
'completed_at' => data_get($activity, 'completed_at'),
'notification_sent_at' => data_get($activity, 'notification_sent_at'),
'notification_status' => data_get($activity, 'notification_status'),
'note_to_customer' => data_get($activity, 'note_to_customer'),
'assignation' => null,
];
});
$activitiesList = $activities->values()->all();
$pendingActivities = $activities
->filter(function (array $activity) {
return empty($activity['completed_at']) && ! empty($activity['estimated_completed_at']);
})
->values()
->all();
$completedActivities = $activities
->filter(function (array $activity) {
return ! empty($activity['completed_at']);
})
->values()
->all();
FacadesLog::info('Hydrated client data', [
'localClient' => $localClient->toArray(),
'assignments' => $assignments,
'users' => $users,
'invoices' => $invoices,
]);
return [
$localClient,
$assignments,
$users,
$invoices,
$activitiesList,
$pendingActivities,
$completedActivities,
$lifeTimeSpend,
$clientAdjustments,
];
}
public function insertCSVDataToDB()
{
$collection = (new FastExcel)->import(storage_path('app/public/csv/Fixed_HJ.csv'));
$array = $collection->toArray();
foreach ($array as $row) {
$startDate = Carbon::parse($row['start_date'])->format('Y-m-d');
$endDate = Carbon::parse($row['end_date'])->format('Y-m-d');
$client = Client::where('customer_id', str_replace('-', '', $row['customer_id']))->first();
// dd($row);
if ($client) {
$salesUser = User::where('name', $row['sales'])->first();
$pic = User::where('name', $row['pic'])->first();
if ($pic) {
ClientUserAssignation::updateOrCreate(
[
'client_id' => $client->id,
'role' => ClientUserAssignation::ROLE_ASSIGNED_PERSON,
],
[
'user_id' => $pic->id,
]
);
}
if ($salesUser) {
ClientUserAssignation::updateOrCreate(
[
'client_id' => $client->id,
'role' => ClientUserAssignation::ROLE_SALES_PERSON,
],
[
'user_id' => $salesUser->id,
]
);
}
$row['client_id'] = $client->id;
$spend = 0;
if ($client->status != 'CANCELED') {
$campaigns = $this->adsService->listCampaigns($row['customer_id']);
FacadesLog::info('Hydrated client data', [
'campaigns' => $campaigns,
]);
foreach ($campaigns as $campaign) {
FacadesLog::info('Hydrated client data', [
'campaigns' => $campaign['id'],
]);
if (empty($startDate) || empty($endDate)) {
continue;
} else {
$metrics = $this->adsService->listCampaignsMetricsById(
$row['customer_id'],
$campaign['id'],
$startDate,
$endDate
);
FacadesLog::info('Hydrated client data', [
'metrics' => $metrics,
]);
$totalSpend = array_sum(array_column($metrics, 'actual_spend'));
$spend += $totalSpend;
}
}
}
ClientInvoice::updateOrCreate(
['invoice_no' => $row['invoice_no']],
[
'client_id' => $row['client_id'],
'is_credit_card' => intval($row['media_fee']) == 0 ? 1 : 0,
'start_date' => $startDate,
'end_date' => $endDate,
'management_fee' => intval($row['management_fee']),
'media_fee' => intval($row['media_fee']),
'tax_percent' => 8,
'nett_amount' => intval($row['media_fee']) > 0 ? intval($row['media_fee']) / 1.08 : 0,
'total_spending' => $spend,
]
);
} else {
FacadesLog::warning('Client not found for customer_id: '.str_replace('-', '', $row['customer_id']));
continue; // Skip this row if client not found
}
}
}
}