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; } }