inspiren-sem-tool/app/Services/ClientInvoicePaymentSyncService.php

286 lines
11 KiB
PHP

<?php
namespace App\Services;
use App\Models\BillingItemType;
use App\Models\ClientInvoice;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;
class ClientInvoicePaymentSyncService
{
public const MEDIA_SEARCH_NAME = 'Google Ads Search (Media Fee)';
public const MANAGEMENT_SEARCH_NAME = 'Management Fee (Google Search Ads)';
public const MANAGEMENT_DEMAND_GEN_NAME = 'Management Fee (Google Demand Gen Ads)';
public const MEDIA_DEMAND_GEN_NAME = 'Google Demand Gen Ads (Media Fee)';
public function sync(ClientInvoice $invoice, array $payments): ClientInvoice
{
return DB::transaction(function () use ($invoice, $payments) {
$manualTotalSemAmount = $invoice->total_sem_amount;
$manualTotalNetAmount = $invoice->total_net_amount;
$billingItemTypes = BillingItemType::withTrashed()
->whereIn('id', collect($payments)->flatMap(fn (array $payment) => $payment['items'] ?? [])
->pluck('billing_item_types_id')
->filter()
->unique()
->values())
->get()
->keyBy('id');
$invoice->payments()->with('items')->get()->each(function ($payment) {
$payment->items()->delete();
$payment->delete();
});
$totals = $this->emptyTotals();
foreach ($payments as $paymentPayload) {
$payment = $invoice->payments()->create([
'payment_no' => $paymentPayload['payment_no'] ?? null,
'payment_total_amount' => $this->amount($paymentPayload['payment_total_amount'] ?? 0),
'payment_nett_amount' => $this->amount($paymentPayload['payment_nett_amount'] ?? 0),
]);
$totals['amount'] += (float) $payment->payment_total_amount;
$totals['payment_no'] ??= $payment->payment_no;
foreach ($paymentPayload['items'] ?? [] as $itemPayload) {
$billingItemType = $billingItemTypes->get((int) ($itemPayload['billing_item_types_id'] ?? 0));
$grossAmount = $this->amount($itemPayload['payment_item_amount'] ?? 0);
$netAmount = $this->amount($itemPayload['net_amount'] ?? 0);
$finalNetAmount = $this->amount($itemPayload['final_net_amount'] ?? $netAmount);
$spending = $this->amount($itemPayload['spending'] ?? 0);
$isCreditCard = (bool) ($itemPayload['is_creditcard'] ?? false);
$payment->items()->create([
'billing_item_types_id' => $billingItemType?->id ?? $itemPayload['billing_item_types_id'],
'start_date' => $itemPayload['start_date'] ?? null,
'end_date' => $itemPayload['end_date'] ?? null,
'payment_item_amount' => $grossAmount,
'tax_percentage' => $this->amount($itemPayload['tax_percentage'] ?? 0),
'net_amount' => $netAmount,
'withholding_tax' => $this->amount($itemPayload['withholding_tax'] ?? 0),
'final_net_amount' => $finalNetAmount,
'spending' => $spending,
'is_creditcard' => $isCreditCard,
]);
$this->addItemToTotals($totals, $billingItemType, [
'gross_amount' => $grossAmount,
'net_amount' => $netAmount,
'final_net_amount' => $finalNetAmount,
'spending' => $spending,
'is_creditcard' => $isCreditCard,
'start_date' => $itemPayload['start_date'] ?? null,
'end_date' => $itemPayload['end_date'] ?? null,
]);
}
}
$invoice->update(
$this->legacyInvoicePayload(
$totals,
$manualTotalSemAmount,
$manualTotalNetAmount,
)
);
return $invoice->refresh()->load('payments.items.billingItemType');
});
}
public function syncLegacyFees(ClientInvoice $invoice, bool $replaceExisting = false): ClientInvoice
{
if (! $replaceExisting && $invoice->payments()->exists()) {
return $invoice;
}
return $this->sync($invoice, $this->legacyPaymentsFor($invoice));
}
public function legacyPaymentsFor(ClientInvoice $invoice): array
{
$this->ensureDefaultItemTypes();
$mediaType = BillingItemType::where('name', self::MEDIA_SEARCH_NAME)->firstOrFail();
$managementType = BillingItemType::where('name', self::MANAGEMENT_SEARCH_NAME)->firstOrFail();
$taxPercentage = (float) ($invoice->tax_percent ?? 0);
$mediaGross = (float) ($invoice->media_fee ?? 0);
$managementGross = (float) ($invoice->management_fee ?? 0);
$mediaNet = (float) ($invoice->media_fee_amount ?? $this->netFromGross($mediaGross, $taxPercentage));
$managementNet = (float) ($invoice->management_fee_amount ?? $this->netFromGross($managementGross, $taxPercentage));
$mediaFinalNet = (float) ($invoice->nett_amount ?? $mediaNet);
$paymentTotal = $mediaGross + $managementGross;
return [[
'payment_no' => $invoice->payment_no,
'payment_total_amount' => $paymentTotal,
'payment_tax_percentage' => $taxPercentage,
'payment_nett_amount' => $this->netFromGross($paymentTotal, $taxPercentage),
// 'items' => [
// [
// 'billing_item_types_id' => $mediaType->id,
// 'start_date' => $invoice->start_date?->toDateString(),
// 'end_date' => $invoice->end_date?->toDateString(),
// 'payment_item_amount' => $mediaGross,
// 'tax_percentage' => $taxPercentage,
// 'net_amount' => $mediaNet,
// 'withholding_tax' => 0,
// 'final_net_amount' => $mediaFinalNet,
// 'spending' => (float) ($invoice->total_spending ?? 0),
// 'is_creditcard' => (bool) $invoice->is_credit_card,
// ],
// [
// 'billing_item_types_id' => $managementType->id,
// 'start_date' => null,
// 'end_date' => null,
// 'payment_item_amount' => $managementGross,
// 'tax_percentage' => $taxPercentage,
// 'net_amount' => $managementNet,
// 'withholding_tax' => 0,
// 'final_net_amount' => $managementNet,
// 'spending' => 0,
// 'is_creditcard' => false,
// ],
// ],
]];
}
public function ensureDefaultItemTypes(): Collection
{
return collect($this->defaultItemTypes())->map(function (array $itemType) {
$billingItemType = BillingItemType::withTrashed()->updateOrCreate(
['name' => $itemType['name']],
$itemType
);
if ($billingItemType->trashed()) {
$billingItemType->restore();
}
return $billingItemType;
});
}
public function defaultItemTypes(): array
{
return [
[
'name' => self::MEDIA_SEARCH_NAME,
'sql_acc_code' => 'G03',
'nett_contribution' => true,
'fee_type' => 'Media',
'type' => 'Google',
'campaign_type' => 'Search',
],
[
'name' => self::MANAGEMENT_SEARCH_NAME,
'sql_acc_code' => 'GOOGLE',
'nett_contribution' => false,
'fee_type' => 'Management',
'type' => 'Google',
'campaign_type' => 'Search',
],
[
'name' => self::MANAGEMENT_DEMAND_GEN_NAME,
'sql_acc_code' => 'M05',
'nett_contribution' => false,
'fee_type' => 'Management',
'type' => 'Google',
'campaign_type' => 'Demand Gen',
],
[
'name' => self::MEDIA_DEMAND_GEN_NAME,
'sql_acc_code' => 'G06',
'nett_contribution' => true,
'fee_type' => 'Media',
'type' => 'Google',
'campaign_type' => 'Demand Gen',
],
];
}
private function addItemToTotals(array &$totals, ?BillingItemType $billingItemType, array $item): void
{
$feeType = strtolower((string) $billingItemType?->fee_type);
if ($feeType === 'media') {
$totals['media_fee'] += $item['gross_amount'];
$totals['media_fee_amount'] += $item['net_amount'];
$totals['media_fee_tax'] += max(0, $item['gross_amount'] - $item['net_amount']);
}
if ($feeType === 'management') {
$totals['management_fee'] += $item['gross_amount'];
$totals['management_fee_amount'] += $item['net_amount'];
$totals['management_fee_tax'] += max(0, $item['gross_amount'] - $item['net_amount']);
}
if ($billingItemType?->nett_contribution) {
$totals['nett_amount'] += $item['final_net_amount'];
}
$totals['total_sem_amount'] += $item['gross_amount'];
$totals['total_spending'] += $item['spending'];
$totals['is_credit_card'] = $totals['is_credit_card'] || $item['is_creditcard'];
if ($item['start_date'] !== null) {
$totals['start_date'] ??= $item['start_date'];
}
if ($item['end_date'] !== null) {
$totals['end_date'] ??= $item['end_date'];
}
}
private function legacyInvoicePayload(
array $totals,
mixed $manualTotalSemAmount = null,
mixed $manualTotalNetAmount = null,
): array
{
return [
'total_sem_amount' => $manualTotalSemAmount ?? $totals['total_sem_amount'],
'total_net_amount' => $manualTotalNetAmount ?? $totals['nett_amount'],
];
}
private function emptyTotals(): array
{
return [
'payment_no' => null,
'start_date' => null,
'end_date' => null,
'amount' => 0,
'management_fee' => 0,
'management_fee_amount' => 0,
'management_fee_tax' => 0,
'media_fee' => 0,
'media_fee_amount' => 0,
'media_fee_tax' => 0,
'nett_amount' => 0,
'total_spending' => 0,
'total_sem_amount' => 0,
'payment_tax_percentage' => null,
'is_credit_card' => false,
];
}
private function netFromGross(float $grossAmount, float $taxPercentage): float
{
return $taxPercentage > 0
? $grossAmount / (1 + ($taxPercentage / 100))
: $grossAmount;
}
private function amount(mixed $amount): float
{
return is_numeric($amount) ? (float) $amount : 0.0;
}
}