Files
api/services/nginx/app/classes/economic_v2_line_normalizer.php
T

291 lines
11 KiB
PHP

<?php
namespace classes;
use helpers\economic_invoice_booked;
use objects\collected_order_invoices_o;
use objects\order_items_o;
use objects\orders_o;
use objects\products_o;
class economic_v2_line_normalizer
{
public static function normalizeInternalCollectedInvoice(collected_order_invoices_o $invoice): array
{
$lines = [];
$warnings = [];
foreach ($invoice->getOrders() as $order_row) {
$order = (new orders_o())->select((int)$order_row['id']);
$department_id = (int)$order->department_id->value();
$order_items = (new order_items_o())->getAllItemsAsArray(
(int)$order->id,
['id', 'product_id', 'reference', 'notes', 'price', 'quantity', 'include_in_invoice']
);
foreach ($order_items as $row) {
if (!(bool)($row['include_in_invoice'] ?? false)) {
continue;
}
$product = (new products_o())->getProductById((int)$row['product_id']);
if (!$product->exists()) {
$warnings[] = 'Missing product for internal order item id ' . (int)$row['id'];
continue;
}
$product_number = $product->economic_product_id->value();
$quantity = (float)$row['quantity'];
$unit_net_price = (float)$row['price'];
$line_net_amount = $quantity * $unit_net_price;
$department_distribution = [$department_id => 100.0];
$line = [
'index' => count($lines),
'source' => 'internal',
'source_order_id' => (int)$order->id,
'source_line_id' => (int)$row['id'],
'line_type' => self::detectLineType($product_number, $unit_net_price, true),
'billable' => true,
'product_number' => $product_number !== null ? (string)$product_number : null,
'product_id' => (int)$row['product_id'],
'description' => (string)$product->name->value(),
'reference' => (string)($row['reference'] ?? ''),
'quantity' => $quantity,
'unit_net_price' => $unit_net_price,
'line_net_amount' => $line_net_amount,
'department_distribution' => $department_distribution,
];
$line['match_key'] = self::buildMatchKey($line);
$lines[] = $line;
}
}
return self::wrap('internal', $lines, $warnings);
}
public static function normalizeDraftInvoice(object|array|null $draft_invoice): array
{
if ($draft_invoice === null) {
return self::wrap('draft', [], ['Draft invoice missing']);
}
$data = self::toArray($draft_invoice);
$raw_lines = is_array($data['lines'] ?? null) ? $data['lines'] : [];
$lines = [];
foreach ($raw_lines as $raw_line) {
$line = self::normalizeEconomicLine($raw_line, 'draft');
$line['index'] = count($lines);
$line['source_line_id'] = isset($raw_line['lineNumber']) ? (int)$raw_line['lineNumber'] : (int)($raw_line['line_number'] ?? count($lines) + 1);
$line['match_key'] = self::buildMatchKey($line);
$lines[] = $line;
}
$wrapped = self::wrap('draft', $lines, []);
if (isset($data['netAmount'])) {
$wrapped['totals']['net_total'] = (float)$data['netAmount'];
} elseif (isset($data['net_amount'])) {
$wrapped['totals']['net_total'] = (float)$data['net_amount'];
}
$wrapped['totals']['difference_from_line_sum'] = round(
(float)$wrapped['totals']['net_total'] - (float)$wrapped['totals']['line_net_total'],
5
);
return $wrapped;
}
public static function normalizeBookedInvoice(object|array|null $booked_invoice): array
{
if ($booked_invoice === null) {
return self::wrap('booked', [], ['Booked invoice missing']);
}
if ($booked_invoice instanceof economic_invoice_booked) {
$data = $booked_invoice->toArray();
} else {
$data = self::toArray($booked_invoice);
}
$raw_lines = is_array($data['lines'] ?? null) ? $data['lines'] : [];
$lines = [];
foreach ($raw_lines as $raw_line) {
$line = self::normalizeEconomicLine($raw_line, 'booked');
$line['index'] = count($lines);
$line['source_line_id'] = (int)($raw_line['lineNumber'] ?? $raw_line['line_number'] ?? count($lines) + 1);
$line['match_key'] = self::buildMatchKey($line);
$lines[] = $line;
}
$wrapped = self::wrap('booked', $lines, []);
if (isset($data['netAmount'])) {
$wrapped['totals']['net_total'] = (float)$data['netAmount'];
} elseif (isset($data['net_amount'])) {
$wrapped['totals']['net_total'] = (float)$data['net_amount'];
}
$wrapped['totals']['difference_from_line_sum'] = round(
(float)$wrapped['totals']['net_total'] - (float)$wrapped['totals']['line_net_total'],
5
);
return $wrapped;
}
private static function normalizeEconomicLine(array $raw_line, string $source): array
{
$line = self::toArray($raw_line);
$product_number = $line['product']['productNumber']
?? $line['product']['product_number']
?? null;
$description = (string)($line['description'] ?? '');
$quantity = isset($line['quantity']) ? (float)$line['quantity'] : 0.0;
$unit_net_price = isset($line['unitNetPrice'])
? (float)$line['unitNetPrice']
: (isset($line['unit_net_price']) ? (float)$line['unit_net_price'] : 0.0);
$line_net_amount = isset($line['totalNetAmount'])
? (float)$line['totalNetAmount']
: (isset($line['total_net_amount']) ? (float)$line['total_net_amount'] : $quantity * $unit_net_price);
$department_distribution = self::extractDepartmentDistribution($line);
$billable = ($product_number !== null) || abs($line_net_amount) > 0.00001 || abs($quantity) > 0.00001;
return [
'source' => $source,
'source_order_id' => null,
'line_type' => self::detectLineType($product_number, $unit_net_price, $billable),
'billable' => $billable,
'product_number' => $product_number !== null ? (string)$product_number : null,
'product_id' => null,
'description' => $description,
'reference' => '',
'quantity' => $quantity,
'unit_net_price' => $unit_net_price,
'line_net_amount' => $line_net_amount,
'department_distribution' => $department_distribution,
];
}
private static function extractDepartmentDistribution(array $line): array
{
$distribution = [];
$dd = $line['departmentalDistribution'] ?? $line['departmental_distribution'] ?? null;
if (is_array($dd)) {
$distributions = $dd['distributions'] ?? null;
if (is_array($distributions)) {
foreach ($distributions as $entry) {
$department_number = $entry['department']['departmentNumber']
?? $entry['department']['department_number']
?? null;
if ($department_number === null) {
continue;
}
$distribution[(string)$department_number] = (float)($entry['percentage'] ?? 0.0);
}
}
if (empty($distribution) && isset($dd['departmentalDistributionNumber'])) {
$distribution[(string)$dd['departmentalDistributionNumber']] = 100.0;
} elseif (empty($distribution) && isset($dd['departmental_distribution_number'])) {
$distribution[(string)$dd['departmental_distribution_number']] = 100.0;
}
}
if (empty($distribution)) {
$distribution['unassigned'] = 100.0;
}
return $distribution;
}
private static function wrap(string $source, array $lines, array $warnings): array
{
$net_total = 0.0;
$line_net_total = 0.0;
$billable_count = 0;
$departments = [];
foreach ($lines as $line) {
$line_net_total += (float)$line['line_net_amount'];
if (!(bool)$line['billable']) {
continue;
}
$billable_count++;
$line_amount = (float)$line['line_net_amount'];
$net_total += $line_amount;
foreach ($line['department_distribution'] as $department_key => $percentage) {
if (!isset($departments[$department_key])) {
$departments[$department_key] = 0.0;
}
$departments[$department_key] += $line_amount * ((float)$percentage / 100);
}
}
return [
'source' => $source,
'totals' => [
'net_total' => round($net_total, 5),
'line_net_total' => round($line_net_total, 5),
'line_count' => count($lines),
'billable_line_count' => $billable_count,
],
'departments' => self::roundMap($departments),
'lines' => $lines,
'warnings' => $warnings,
];
}
private static function buildMatchKey(array $line): string
{
if (!empty($line['product_number'])) {
return 'product:' . strtolower(trim((string)$line['product_number'])) .
'|ref:' . strtolower(trim((string)($line['reference'] ?? '')));
}
return 'text:' . self::normalizeText((string)$line['description']);
}
private static function detectLineType(mixed $product_number, float $unit_net_price, bool $billable): string
{
if (!$billable) {
return 'text';
}
if ($product_number !== null && strtolower((string)$product_number) === 'totdiscount') {
return 'discount';
}
if ($unit_net_price < 0) {
return 'discount';
}
return $product_number !== null ? 'product' : 'text';
}
private static function normalizeText(string $text): string
{
$text = trim(strtolower($text));
$text = preg_replace('/\s+/', ' ', $text);
return $text ?? '';
}
private static function roundMap(array $map): array
{
$rounded = [];
foreach ($map as $k => $v) {
$rounded[(string)$k] = round((float)$v, 5);
}
return $rounded;
}
private static function toArray(object|array $value): array
{
if (is_array($value)) {
return $value;
}
return json_decode(json_encode($value, JSON_UNESCAPED_UNICODE), true) ?: [];
}
}