inspiren-sem-tool/app/Console/Commands/CreateClientInvoice.php

378 lines
12 KiB
PHP

<?php
namespace App\Console\Commands;
use App\Models\Client;
use App\Models\ClientInvoice;
use App\Models\ClientUserAssignation;
use App\Models\User;
use App\Services\ClientInvoicePaymentSyncService;
use Carbon\Carbon;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
use Rap2hpoutre\FastExcel\FastExcel;
class CreateClientInvoice extends Command
{
protected $signature = 'customer:create-invoice';
protected $description = 'Create client invoice';
public function handle()
{
$paymentSyncService = app(ClientInvoicePaymentSyncService::class);
try {
DB::beginTransaction();
$collection = (new FastExcel)->import(storage_path('app/public/csv/Fixed_EJ.csv'));
// $collection = (new FastExcel)->import(storage_path('app/public/csv/Fixed_HJ.csv'));
// $collection = (new FastExcel)->import(storage_path('app/public/csv/Fixed_K.csv'));
$array = $collection->toArray();
$linkedInvoices = [];
$linkedInvoiceTargets = $this->linkedInvoiceTargets($array);
foreach ($this->groupRowsByInvoice($array) as $invoiceNo => $invoiceRows) {
$invoiceClient = null;
$linkedInvoiceNo = null;
$payments = [];
$totalSemAmount = 0.0;
$totalNetAmount = 0.0;
foreach ($invoiceRows as $row) {
$customerId = str_replace('-', '', (string) $this->rowValue($row, 'customer_id', ''));
$startDate = $this->date($this->rowValue($row, 'start_date'));
$endDate = $this->date($this->rowValue($row, 'end_date'));
$client = Client::where('customer_id', $customerId)->first();
if (! $client) {
Log::warning('Client not found for customer_id: '.$customerId, [
'invoice_no' => $invoiceNo,
]);
continue;
}
if ($invoiceClient !== null && $invoiceClient->isNot($client)) {
Log::warning('Invoice rows resolve to different clients; row skipped.', [
'invoice_no' => $invoiceNo,
'expected_client_id' => $invoiceClient->id,
'row_client_id' => $client->id,
]);
continue;
}
$invoiceClient ??= $client;
$client->update([
'industry' => $this->rowValue($row, 'industry'),
]);
$salesUser = User::where('name', $this->rowValue($row, 'sales'))->first();
$pic = User::where('name', $this->rowValue($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,
]
);
}
$rowLinkedInvoiceNo = $this->linkedInvoiceNo($this->rowValue($row, 'linked_invoice_no', ''));
$managementFee = $this->amount($this->rowValue($row, 'management_fee', 0));
$mediaFee = $this->amount($this->rowValue($row, 'media_fee', 0));
$paymentNettAmount = $mediaFee + $managementFee;
$tax = $this->tax($this->rowValue($row, 'tax', 0), $paymentNettAmount, $startDate);
$paymentTotalAmount = $paymentNettAmount + $tax['amount'];
$isCreditCard = $mediaFee == 0 && ! in_array($invoiceNo, $linkedInvoiceTargets, true);
$payments[] = [
'payment_no' => $this->nullableString($this->rowValue($row, 'payment_no')),
'payment_total_amount' => $paymentTotalAmount,
'payment_nett_amount' => $paymentNettAmount,
'items' => $this->paymentItems(
$mediaFee,
$managementFee,
$tax['percentage'],
$startDate,
$endDate,
$isCreditCard,
),
];
$totalSemAmount += $paymentTotalAmount;
$totalNetAmount += $paymentNettAmount;
if ($rowLinkedInvoiceNo !== null) {
$linkedInvoiceNo ??= $rowLinkedInvoiceNo;
}
}
if ($invoiceClient === null || $payments === []) {
continue;
}
$invoice = ClientInvoice::updateOrCreate(
['invoice_no' => $invoiceNo],
[
'client_id' => $invoiceClient->id,
'approved_at' => now(),
'total_sem_amount' => $totalSemAmount,
'total_net_amount' => $totalNetAmount,
]
);
$paymentSyncService->sync($invoice, $payments);
if ($linkedInvoiceNo !== null) {
$linkedInvoices[$invoiceNo] = $linkedInvoiceNo;
}
}
foreach ($linkedInvoices as $invoiceNo => $linkedInvoiceNo) {
$invoice = ClientInvoice::where('invoice_no', $invoiceNo)->first();
$linkedInvoice = ClientInvoice::where('invoice_no', $linkedInvoiceNo)->first();
if ($invoice && $linkedInvoice) {
$invoice->update([
'linked_invoice_id' => $linkedInvoice->id,
]);
}
}
DB::commit();
} catch (\Exception $e) {
DB::rollBack();
Log::error('Error project linkage : '.$e->getMessage(), [
'trace' => $e->getTraceAsString(),
]);
return 1;
}
return 0;
}
private function rowValue(array $row, string $key, mixed $default = null): mixed
{
$normalizedKey = $this->normalizeHeader($key);
foreach ($row as $rowKey => $value) {
if ($this->normalizeHeader((string) $rowKey) === $normalizedKey) {
return $value;
}
}
return $default;
}
private function normalizeHeader(string $header): string
{
return trim(preg_replace('/[^a-z0-9]+/', '_', strtolower($header)), '_');
}
private function nullableString(mixed $value): ?string
{
$value = trim((string) $value);
return $value === '' ? null : $value;
}
private function amount(mixed $value): float
{
$normalized = preg_replace('/[^0-9.\-]/', '', (string) $value);
return is_numeric($normalized) ? (float) $normalized : 0.0;
}
private function invoiceNo(mixed $value): string
{
$invoiceNo = trim((string) $value);
if (preg_match('/^\s*([A-Za-z0-9]+)/', $invoiceNo, $matches)) {
return $matches[1];
}
return $invoiceNo;
}
private function linkedInvoiceNo(mixed $value): ?string
{
$linkedInvoiceNo = $this->invoiceNo($value);
if ($linkedInvoiceNo === '' || $linkedInvoiceNo === '0') {
return null;
}
return $linkedInvoiceNo;
}
/**
* @param array<int, array<string, mixed>> $rows
* @return array<string, array<int, array<string, mixed>>>
*/
private function groupRowsByInvoice(array $rows): array
{
$groupedRows = [];
foreach ($rows as $row) {
$invoiceNo = $this->invoiceNo($this->rowValue($row, 'invoice_no'));
if ($invoiceNo === '') {
Log::warning('Invoice row skipped because invoice_no is empty.');
continue;
}
$groupedRows[$invoiceNo][] = $row;
}
return $groupedRows;
}
/**
* @param array<int, array<string, mixed>> $rows
* @return array<int, string>
*/
private function linkedInvoiceTargets(array $rows): array
{
$invoiceNumbers = [];
foreach ($rows as $row) {
$linkedInvoiceNo = $this->linkedInvoiceNo($this->rowValue($row, 'linked_invoice_no', ''));
if ($linkedInvoiceNo !== null) {
$invoiceNumbers[] = $linkedInvoiceNo;
}
}
return array_values(array_unique($invoiceNumbers));
}
private function date(mixed $value): ?Carbon
{
if (empty($value)) {
return null;
}
$date = trim((string) $value);
foreach (['d/m/y', 'd/m/Y', 'Y-m-d', 'd-m-y', 'd-m-Y'] as $format) {
try {
return Carbon::createFromFormat($format, $date)->startOfDay();
} catch (\Throwable) {
continue;
}
}
return Carbon::parse($date)->startOfDay();
}
/**
* @return array{percentage: float, amount: float}
*/
private function tax(mixed $value, float $amount, ?Carbon $startDate): array
{
$tax = $this->amount($value);
if ($tax > 100 && $amount > 0) {
return [
'percentage' => ($tax / $amount) * 100,
'amount' => $tax,
];
}
if ($tax > 0) {
return [
'percentage' => $tax,
'amount' => $this->taxAmount($amount, $tax),
];
}
if ($startDate !== null && $startDate->greaterThanOrEqualTo(Carbon::parse('2024-03-01'))) {
return [
'percentage' => 8,
'amount' => $this->taxAmount($amount, 8),
];
}
return [
'percentage' => 6,
'amount' => $this->taxAmount($amount, 6),
];
}
private function taxAmount(float $amount, float $taxPercentage): float
{
return $amount * ($taxPercentage / 100);
}
/**
* @return array<int, array<string, mixed>>
*/
private function paymentItems(
float $mediaFee,
float $managementFee,
float $taxPercentage,
?Carbon $startDate,
?Carbon $endDate,
bool $isCreditCard,
): array {
$items = [];
if ($mediaFee > 0 || $isCreditCard) {
$mediaTax = $this->taxAmount($mediaFee, $taxPercentage);
$items[] = [
'billing_item_types_id' => 1,
'start_date' => $startDate?->format('Y-m-d'),
'end_date' => $endDate?->format('Y-m-d'),
'payment_item_amount' => $mediaFee + $mediaTax,
'tax_percentage' => $taxPercentage,
'net_amount' => $mediaFee,
'withholding_tax' => $taxPercentage,
'final_net_amount' => $this->finalNetAmount($mediaFee, $taxPercentage),
'spending' => 0,
'is_creditcard' => $isCreditCard,
];
}
if ($managementFee > 0) {
$managementTax = $this->taxAmount($managementFee, $taxPercentage);
$items[] = [
'billing_item_types_id' => 2,
'start_date' => null,
'end_date' => null,
'payment_item_amount' => $managementFee + $managementTax,
'tax_percentage' => $taxPercentage,
'net_amount' => $managementFee,
'withholding_tax' => 0,
'final_net_amount' => $this->finalNetAmount($managementFee, 0),
'spending' => 0,
'is_creditcard' => false,
];
}
return $items;
}
private function finalNetAmount(float $netAmount, float $withholdingTax): float
{
return $netAmount / (1 + ($withholdingTax / 100));
}
}