286 lines
11 KiB
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;
|
|
}
|
|
}
|