378 lines
12 KiB
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));
|
|
}
|
|
}
|