Implement subuser verification and invoice/self-serve API fixes

This commit is contained in:
Jeppe Bundgaard
2026-07-13 15:11:49 +02:00
parent a4fafaf7fb
commit 582edd3e6c
29 changed files with 2215 additions and 124 deletions
@@ -0,0 +1,28 @@
<?php
namespace config;
use traits\module_config_variable;
class economic_invoice_discount_layout_c
{
use module_config_variable;
/**
* @throws \Exception
*/
public function __construct()
{
self::setupConfigVariable(
'economic',
'invoiceDiscountLayoutNumber',
'int',
false,
null,
'The duplicated invoice layout number used when collected invoices include itemized discounts',
'1',
false,
null
);
}
}
@@ -1,5 +1,6 @@
<?php
require_once WD . '/modules/economic/config/economic_invoice_layout_c.php';
require_once WD . '/modules/economic/config/economic_invoice_discount_layout_c.php';
require_once WD . '/modules/economic/config/economic_payment_terms_c.php';
require_once WD . '/modules/economic/config/economic_admin_fee_monthly_c.php';
require_once WD . '/modules/economic/config/economic_admin_fee_order_c.php';
@@ -8,6 +9,7 @@ require_once WD . '/modules/economic/config/economic_transaction_draft_customer_
require_once WD . '/modules/economic/config/economic_default_department_id_c.php';
use config\economic_invoice_layout_c;
use config\economic_invoice_discount_layout_c;
use config\economic_payment_terms_c;
use config\economic_admin_fee_monthly_c;
use config\economic_admin_fee_order_c;
@@ -21,6 +23,7 @@ class economic_c
use module_config_t;
public economic_invoice_layout_c $invoice_layout;
public economic_invoice_discount_layout_c $invoice_discount_layout;
public economic_payment_terms_c $payment_terms;
public economic_admin_fee_monthly_c $admin_fee_monthly;
public economic_admin_fee_order_c $admin_fee_order;
@@ -33,6 +36,7 @@ class economic_c
$this->setupConfig('economic');
$this->allowUpdate([
economic_invoice_layout_c::class,
economic_invoice_discount_layout_c::class,
economic_payment_terms_c::class,
economic_admin_fee_monthly_c::class,
economic_admin_fee_order_c::class,
@@ -41,6 +45,7 @@ class economic_c
economic_transaction_draft_customer_number_c::class,
]);
$this->invoice_layout = new economic_invoice_layout_c();
$this->invoice_discount_layout = new economic_invoice_discount_layout_c();
$this->payment_terms = new economic_payment_terms_c();
$this->admin_fee_monthly = new economic_admin_fee_monthly_c();
$this->admin_fee_order = new economic_admin_fee_order_c();
@@ -72,7 +72,7 @@ class economic_invoices_draft_endpoint
* @return array{order_count:int,orders_with_invoice_lines:int,line_count:int,batch_count:int,batch_sizes:array<int,int>}
* @throws Exception If the request fails
*/
public function add_orders(int $invoiceDraftId, array $orders, string $currency = 'DKK', int $line_batch_size = 500): array
public function add_orders(int $invoiceDraftId, array $orders, string $currency = 'DKK', int $line_batch_size = 500, bool $use_itemized_discounts = false): array
{
$draftInvoice = (new economic())->getInvoiceDraft($invoiceDraftId, strtoupper($currency), true);
$orders_with_invoice_lines = 0;
@@ -90,7 +90,7 @@ class economic_invoices_draft_endpoint
// Add the transaction header (Timestamp, department, etc.)
$draftInvoice->addNewTransactionHeader($order);
// Add the order lines
$draftInvoice->addOrderItemLines($order);
$draftInvoice->addOrderItemLines($order, $use_itemized_discounts);
// Add an empty line, so the invoice is not empty
$draftInvoice->addTextLine('');
}
@@ -104,10 +104,12 @@ class economic_invoices_drafts_endpoint
* @return object {id: number, getExternalId: string}
* @throws Exception If the request fails
*/
public function add(int $customer_number, string $external_id = '', string|null $date = null): object
public function add(int $customer_number, string $external_id = '', string|null $date = null, ?int $layout_number = null): object
{
// Get the layout number
$layout_number = (int)(new economic())->config->invoice_layout->getVariableValue();
$layout_number = $layout_number !== null && $layout_number > 0
? $layout_number
: (int)(new economic())->config->invoice_layout->getVariableValue();
// Check if the date is set
if (is_null($date)) {
@@ -249,7 +249,7 @@ class economic_invoice_draft
* @throws Exception if the order is not found
* @throws Exception if the order is not valid
*/
public function addOrderItemLines(orders_o $order): void
public function addOrderItemLines(orders_o $order, bool $use_itemized_discounts = false): void
{
// Get the order items
$order_items = $order->getOrderItems($order->id);
@@ -274,12 +274,12 @@ class economic_invoice_draft
continue;
}
// Add the order item to the draft invoice
self::addOrderItemLine($order_item, $department);
self::addOrderItemLine($order_item, $department, false, $use_itemized_discounts);
// Add the line discount to the total discount
$total_discount += ($order_item['product']['price'] - $order_item['price']) * $order_item['quantity'];
}
// If the total discount is greater than 0, add it to the invoice
if ($total_discount > 0) {
if (!$use_itemized_discounts && $total_discount > 0) {
// Add the discount to the invoice
self::addProductDiscountLine($total_discount, $department['economic_department_id'] ?? 0, $department['dimension'] ?? 0);
}
@@ -295,7 +295,7 @@ class economic_invoice_draft
* @throws Exception if the order item is not found
* @throws Exception if the order item is not valid
*/
public function addOrderItemLine(array $order_item, array $department, bool $show_discount = false): void
public function addOrderItemLine(array $order_item, array $department, bool $show_discount = false, bool $use_itemized_discount = false): void
{
// Check if the order item is valid
if (!isset($order_item['id'])) {
@@ -308,14 +308,21 @@ class economic_invoice_draft
$economic_department_id = $department['economic_department_id'] ?? 0;
// Get the dimension id
$economic_dimension_id = $department['economic_dimension_id'] ?? 0;
$pricing = self::resolveOrderItemInvoicePricing($order_item);
$discount_percentage = $use_itemized_discount
? $this->resolveItemizedDiscountPercentageForInvoiceCurrency($pricing)
: 0;
// Add the order item to the draft invoice
self::addProductLine(
(string)$order_item['product']['economic_product_id'],
(string)$order_item['product']['name'],
(int)$order_item['quantity'] ?? 1,
(int)($order_item['product']['price'] == 0 ? $order_item['price'] : $order_item['product']['price']),
$use_itemized_discount
? $pricing['invoice_unit_price']
: (int)($order_item['product']['price'] == 0 ? $order_item['price'] : $order_item['product']['price']),
(int)$economic_department_id,
$economic_dimension_id
$economic_dimension_id,
$discount_percentage
);
// Calculate the discount percentage (If the final price is 0, set the discount percentage to 100)
@@ -378,11 +385,90 @@ class economic_invoice_draft
return abs($price) < 0.00001;
}
/**
* Return exact line discount data from the original department price and stored final item price.
*
* @return array{
* original_unit_price:float,
* final_unit_price:float,
* invoice_unit_price:float,
* quantity:float,
* discount_unit_amount:float,
* discount_total_amount:float,
* discount_percentage:float,
* has_discount:bool
* }
*/
public static function resolveOrderItemInvoicePricing(array $order_item): array
{
$quantity = isset($order_item['quantity']) && is_numeric($order_item['quantity'])
? (float)$order_item['quantity']
: 0.0;
$final_unit_price = isset($order_item['price']) && is_numeric($order_item['price'])
? (float)$order_item['price']
: 0.0;
$original_unit_price = isset($order_item['product']['price']) && is_numeric($order_item['product']['price'])
? (float)$order_item['product']['price']
: 0.0;
if (abs($original_unit_price) < 0.00001) {
$original_unit_price = $final_unit_price;
}
$has_discount = $quantity > 0.0
&& $final_unit_price > 0.0
&& $original_unit_price > 0.0
&& $final_unit_price < ($original_unit_price - 0.00001);
$discount_unit_amount = $has_discount
? round($original_unit_price - $final_unit_price, 2)
: 0.0;
$discount_total_amount = round($discount_unit_amount * $quantity, 2);
return [
'original_unit_price' => $original_unit_price,
'final_unit_price' => $final_unit_price,
'invoice_unit_price' => $has_discount ? $original_unit_price : $final_unit_price,
'quantity' => $quantity,
'discount_unit_amount' => $discount_unit_amount,
'discount_total_amount' => $discount_total_amount,
'discount_percentage' => $has_discount
? round((1 - ($final_unit_price / $original_unit_price)) * 100, 10)
: 0.0,
'has_discount' => $has_discount,
];
}
public static function orderItemHasBillableDiscount(array $order_item): bool
{
return self::resolveOrderItemInvoicePricing($order_item)['has_discount'];
}
/**
* Calculate the hidden e-conomic percentage from converted amounts so the rendered monetary discount stays exact.
*
* @param array{invoice_unit_price:float,final_unit_price:float,has_discount:bool,discount_percentage:float} $pricing
*/
private function resolveItemizedDiscountPercentageForInvoiceCurrency(array $pricing): float
{
if (!$pricing['has_discount']) {
return 0.0;
}
$converted_original = self::convertCurrency($pricing['invoice_unit_price']);
$converted_final = self::convertCurrency($pricing['final_unit_price']);
if ($converted_original <= 0.0 || $converted_final <= 0.0) {
return $pricing['discount_percentage'];
}
return round((1 - ($converted_final / $converted_original)) * 100, 10);
}
/**
* Add a product line to the draft invoice
* @note The lines won't be saved until the addLines() method is called.
*/
public function addProductLine(string $productNumber, string $description, float $quantity, float $unitNetPrice, int $economic_department_id, int $dimension): void
public function addProductLine(string $productNumber, string $description, float $quantity, float $unitNetPrice, int $economic_department_id, int $dimension, float $discountPercentage = 0): void
{
// We then add a 0 in front of the product number to make sure it's the right one, made by FlexPOS.
// Add a line to the invoice
@@ -392,7 +478,7 @@ class economic_invoice_draft
],
'quantity' => $quantity,
'unitNetPrice' => self::convertCurrency($unitNetPrice),
'discountPercentage' => 0,
'discountPercentage' => $discountPercentage,
'description' => $description,
];
// If the department is set, add it to the line
@@ -297,7 +297,25 @@ trait selfserve_lane_invoice_t
$billing_customer_number > 0 ? $billing_customer_number : null,
$subuser_id
);
return $session->exists() ? $session : null;
if ($session->exists()) {
return $session;
}
if ($subuser_id === null) {
return null;
}
$session = (new selfserve_wash_sessions_o())->selectLatestOpenByLaneAndReg(
(int)$this->id,
selfserve::standardize_registration($license_plate),
$billing_customer_number > 0 ? $billing_customer_number : null
);
if (!$session->exists()) {
return null;
}
$sessionSubuserId = $session->subuser_id->value() === null ? null : (int)$session->subuser_id->value();
return $sessionSubuserId === null || $sessionSubuserId === $subuser_id ? $session : null;
} catch (\Throwable) {
return null;
}