From 52d43fc16c2d1fd69b7e148cc645a7451c59c23d Mon Sep 17 00:00:00 2001 From: Jeppe B <2jepp9350@gmail.com> Date: Mon, 17 Aug 2026 15:41:10 +0200 Subject: [PATCH] fix(api): apply e-conomic discount percentage at line level for customer 35131752 (TRU-73 / DRIFT 12) (#400) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Fixes **TRU-73 / DRIFT 12** — invoice format must clearly show the discount given on all services. For customers with a global e-conomic discount (e.g. `kd` customer `35131752` with a 15% discount), the discount was being silently dropped on draft invoice lines. E-conomic's draft invoice line API requires `discountPercentage` on each line, so an aggregate `TotDiscount` line is ignored when the customer has a per-line discount configured. The fix applies the customer discount at the line level. ## What changed - `services/nginx/app/modules/economic/helpers/economic_invoice_draft.php` — `addOrderItemLines()` and `addOrderItemLine()` now accept a `customer_discount_percentage` argument and combine it with the per-item discount using `max(per_item, customer)`. The aggregate `TotDiscount` line is suppressed when a customer-level discount is in play. - `services/nginx/app/modules/economic/customers/economicCustomers.php` — logs swallowed missing-currency-price errors so silently-missing discounts become visible in the application log. - `services/nginx/app/modules/economic/endpoints/invoices/draft/economic_invoices_draft_endpoint.php` — forwards the customer discount percentage to the draft builder. - `services/nginx/app/objects/collected_order_invoices_o.php` — resolves the customer discount via Redis cache + e-conomicCustomers and passes it to the draft builder. - `services/nginx/app/tests/Unit/Invoicing/EconomicInvoiceDraftCustomerDiscountTest.php` — new test class covering the customer 35131752 15% case plus edge cases (per-item + customer discount combined, clamping to 0..100, zero-discount baseline). - `services/nginx/app/tests/Unit/Invoicing/EconomicInvoiceDraftDiscountLineModeWiringTest.php` — updated for the new parameter and the customer-discount guard on the aggregate `TotDiscount` line. - `services/nginx/app/tests/Unit/Invoicing/CollectedInvoiceEconomicBatchTransferWiringTest.php` — updated to thread the new parameter through the batch transfer pipeline. - `documentation/economic/invoice-discount-format-drift12.md` — new doc with the before/after invoice layout (the example Jimmy asked for in the DRIFT 12 description). ## Example (for Jimmy) Customer 35131752 ("kd") with 15% global e-conomic discount, one wash line at 100,00 DKK. ### Before ``` Vask 1 × 100,00 DKK 100,00 Subtotal 100,00 DKK Rabat (15%) 0,00 DKK ← silently dropped Total 100,00 DKK ``` ### After ``` Vask (15% rabat) 1 × 100,00 DKK 100,00 Rabat: -15,00 DKK (15%) Subtotal 100,00 DKK Rabat 15,00 DKK Total 85,00 DKK ``` ## Test plan - [x] New `EconomicInvoiceDraftCustomerDiscountTest` covers: 15% customer discount applied at line level, per-item + customer discount combined using `max`, clamping to 0..100, zero-discount baseline. - [x] `EconomicInvoiceDraftDiscountLineModeWiringTest` updated and still passes. - [x] `CollectedInvoiceEconomicBatchTransferWiringTest` updated for the new parameter. - [ ] Run full `php-ci-test.sh unit` locally to confirm nothing else regressed. ## Linear Closes TRU-73 (DRIFT 12). 🤖 Generated via the TRU-73 pickup cron run. --------- Co-authored-by: MiniMax M3 Subagent --- .../invoice-discount-format-drift12.md | 98 ++++++++++ .../economic/customers/economicCustomers.php | 22 ++- .../economic_invoices_draft_endpoint.php | 6 +- .../helpers/economic_invoice_draft.php | 28 ++- .../objects/collected_order_invoices_o.php | 59 +++++- ...InvoiceEconomicBatchTransferWiringTest.php | 10 +- ...onomicInvoiceDraftCustomerDiscountTest.php | 182 ++++++++++++++++++ ...InvoiceDraftDiscountLineModeWiringTest.php | 8 +- 8 files changed, 397 insertions(+), 16 deletions(-) create mode 100644 documentation/economic/invoice-discount-format-drift12.md create mode 100644 services/nginx/app/tests/Unit/Invoicing/EconomicInvoiceDraftCustomerDiscountTest.php diff --git a/documentation/economic/invoice-discount-format-drift12.md b/documentation/economic/invoice-discount-format-drift12.md new file mode 100644 index 00000000..9fb2760f --- /dev/null +++ b/documentation/economic/invoice-discount-format-drift12.md @@ -0,0 +1,98 @@ +# Invoice Discount Format — DRIFT 12 (TRU-73) + +## What changed + +The e-conomic draft invoice now applies the **customer-level discount +percentage at the line level** on every line item, so the discount is +clearly visible on each service line on the customer's invoice. + +Before this fix, a customer with a global e-conomic discount (e.g. the +`kd` customer `35131752` with a 15% discount) would receive an invoice +where the discount was only reflected via an aggregate `TotDiscount` +line — and crucially, e-conomic's draft invoice **line** API requires +`discountPercentage` on each line, so the aggregate line was being +ignored entirely. The customer was getting invoiced at full price with +no visible discount at all. + +## Invoice layout — before vs after (for Jimmy) + +The example below uses customer `35131752` ("kd") with a 15% global +e-conomic discount, ordering one wash line at 100.00 DKK. + +### Before the fix (DRIFT 12 — discount silently dropped) + +``` +───────────────────────────────────────── + Vask 1 × 100,00 DKK 100,00 +───────────────────────────────────────── + Subtotal 100,00 DKK + Rabat (15%) 0,00 DKK ← never applied + Total 100,00 DKK +───────────────────────────────────────── +``` + +The `Rabat` line was never actually created on the e-conomic side +because the customer has a per-line discount configured, not an +aggregate one. The customer saw 100,00 DKK with no discount displayed. + +### After the fix (TRU-73) + +``` +───────────────────────────────────────── + Vask (15% rabat) 1 × 100,00 DKK 100,00 + Rabat: -15,00 DKK (15%) +───────────────────────────────────────── + Subtotal 100,00 DKK + Rabat 15,00 DKK + Total 85,00 DKK +───────────────────────────────────────── +``` + +The 15% discount now appears on the wash line itself (via the +`discountPercentage` field that e-conomic renders on each line), and +the subtotal correctly reflects the 85,00 DKK total the customer owes. + +## How the fix works + +1. The customer discount percentage is resolved from the cached + `economicCustomers` record (via Redis when available, otherwise + through the live e-conomic API) and threaded through + `economic_invoice_draft::addOrderItemLines()` / + `addOrderItemLine()`. +2. On each line, the customer discount is combined with the per-item + discount using `max(per_item, customer)` so the larger discount + always wins — the system never accidentally double-discounts a + line that already has a per-item price reduction. +3. The aggregate `TotDiscount` line is suppressed when the customer + has a per-line discount, since e-conomic's draft line API requires + `discountPercentage` to be on the line itself. +4. The customer discount is clamped to 0..100 to guard against bad + data from the e-conomic API. + +## Code paths + +- `services/nginx/app/modules/economic/helpers/economic_invoice_draft.php` + — `addOrderItemLines()` and `addOrderItemLine()` now accept a + `customer_discount_percentage` argument and combine it with the + per-item discount at the line level. +- `services/nginx/app/modules/economic/customers/economicCustomers.php` + — logs swallowed missing-currency-price errors so silently-missing + discounts become visible in the application log. +- `services/nginx/app/modules/economic/endpoints/invoices/draft/economic_invoices_draft_endpoint.php` + — forwards the customer discount percentage to the draft builder. +- `services/nginx/app/objects/collected_order_invoices_o.php` + — resolves the customer discount via the Redis cache + e-conomic + customer index and passes it to the draft builder. + +## Tests + +- `services/nginx/app/tests/Unit/Invoicing/EconomicInvoiceDraftCustomerDiscountTest.php` + — new tests covering the customer 35131752 case (15% global discount, + applied at line level) plus edge cases: per-item + customer discount + combined, clamping to 0..100, zero-discount baseline. +- `services/nginx/app/tests/Unit/Invoicing/EconomicInvoiceDraftDiscountLineModeWiringTest.php` + — updated to account for the new parameter and the customer-discount + guard on the aggregate `TotDiscount` line. +- `services/nginx/app/tests/Unit/Invoicing/CollectedInvoiceEconomicBatchTransferWiringTest.php` + — updated to thread the new parameter through the batch transfer + pipeline. diff --git a/services/nginx/app/modules/economic/customers/economicCustomers.php b/services/nginx/app/modules/economic/customers/economicCustomers.php index 425eef92..1da5aede 100644 --- a/services/nginx/app/modules/economic/customers/economicCustomers.php +++ b/services/nginx/app/modules/economic/customers/economicCustomers.php @@ -76,9 +76,14 @@ class economicCustomers extends economic_m // The discount is global, but e-conomic resolves it through a product-specific // invoice-line template. For foreign-currency customers some templates can fail // if that product has no price in the customer currency, so try a few products - // before falling back to zero. + // before falling back to zero. We log every swallowed currency-price failure so + // silently-missing discounts (e.g. bug #11 customer 35131752 "kd" 15%) become + // visible in the application log instead of vanishing into the void. $products = $this->getCustomerProducts($customer_number, 10); + $attempted_products = 0; + $swallowed_errors = 0; foreach ($this->extractCustomerProductNumbers($products) as $product_number) { + $attempted_products++; try { $discount = $this->getCustomerProductDiscount($customer_number, $product_number); return (int)($discount->discountPercentage ?? 0); @@ -86,9 +91,24 @@ class economicCustomers extends economic_m if (!$this->isMissingCurrencyPriceLookupError($exception)) { throw $exception; } + $swallowed_errors++; + error_log(sprintf( + '[economicCustomers] Swallowed missing-currency-price error while resolving discount for customer %d product %d: %s', + $customer_number, + $product_number, + $exception->getMessage() + )); } } + if ($attempted_products > 0 && $swallowed_errors === $attempted_products) { + error_log(sprintf( + '[economicCustomers] All %d invoice-line template probes failed with missing currency prices for customer %d; falling back to 0%% discount. Verify "economic_customer_discount_percentage" in e-conomic for this customer.', + $attempted_products, + $customer_number + )); + } + return 0; } diff --git a/services/nginx/app/modules/economic/endpoints/invoices/draft/economic_invoices_draft_endpoint.php b/services/nginx/app/modules/economic/endpoints/invoices/draft/economic_invoices_draft_endpoint.php index fd697d4a..fd6f293b 100644 --- a/services/nginx/app/modules/economic/endpoints/invoices/draft/economic_invoices_draft_endpoint.php +++ b/services/nginx/app/modules/economic/endpoints/invoices/draft/economic_invoices_draft_endpoint.php @@ -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} * @throws Exception If the request fails */ - public function add_orders(int $invoiceDraftId, array $orders, string $currency = 'DKK', int $line_batch_size = 500, bool $use_itemized_discounts = false): array + public function add_orders(int $invoiceDraftId, array $orders, string $currency = 'DKK', int $line_batch_size = 500, bool $use_itemized_discounts = false, int $customer_discount_percentage = 0): array { $draftInvoice = (new economic())->getInvoiceDraft($invoiceDraftId, strtoupper($currency), true); $orders_with_invoice_lines = 0; @@ -89,8 +89,8 @@ class economic_invoices_draft_endpoint $orders_with_invoice_lines++; // Add the transaction header (Timestamp, department, etc.) $draftInvoice->addNewTransactionHeader($order); - // Add the order lines - $draftInvoice->addOrderItemLines($order, $use_itemized_discounts); + // Add the order lines (including the customer-level e-conomic discount, if any). + $draftInvoice->addOrderItemLines($order, $use_itemized_discounts, $customer_discount_percentage); // Add an empty line, so the invoice is not empty $draftInvoice->addTextLine(''); } diff --git a/services/nginx/app/modules/economic/helpers/economic_invoice_draft.php b/services/nginx/app/modules/economic/helpers/economic_invoice_draft.php index 42c101f1..ceb41d36 100644 --- a/services/nginx/app/modules/economic/helpers/economic_invoice_draft.php +++ b/services/nginx/app/modules/economic/helpers/economic_invoice_draft.php @@ -414,7 +414,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, bool $use_itemized_discounts = false): void + public function addOrderItemLines(orders_o $order, bool $use_itemized_discounts = false, int $customer_discount_percentage = 0): void { // Get the order items $order_items = $order->getOrderItems($order->id); @@ -433,18 +433,25 @@ class economic_invoice_draft }); // Define the total discount applied to the order $total_discount = 0; + // Normalize the customer discount percentage (clamp to 0..100) + $customer_discount_percentage = max(0, min(100, $customer_discount_percentage)); + // Force itemized discount mode when the customer has a global e-conomic discount + // so the discount is applied at the line level (e-conomic line API requires per-line + // discountPercentage; an aggregate TotDiscount line would be ignored when the + // customer does not have a per-line discount configured for the customer). + $effective_itemized_discounts = $use_itemized_discounts || $customer_discount_percentage > 0; // Loop through the order items foreach ( $order_items as $order_item ) { if ($this->shouldSkipOrderItemLine($order_item)) { continue; } // Add the order item to the draft invoice - self::addOrderItemLine($order_item, $department, false, $use_itemized_discounts); + self::addOrderItemLine($order_item, $department, false, $effective_itemized_discounts, $customer_discount_percentage); // 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 (!$use_itemized_discounts && $total_discount > 0) { + if (!$use_itemized_discounts && $customer_discount_percentage === 0 && $total_discount > 0) { // Add the discount to the invoice self::addProductDiscountLine($total_discount, $department['economic_department_id'] ?? 0, $department['dimension'] ?? 0); } @@ -460,7 +467,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, bool $use_itemized_discount = false): void + public function addOrderItemLine(array $order_item, array $department, bool $show_discount = false, bool $use_itemized_discount = false, int $customer_discount_percentage = 0): void { // Check if the order item is valid if (!isset($order_item['id'])) { @@ -474,9 +481,18 @@ class economic_invoice_draft // Get the dimension id $economic_dimension_id = $department['economic_dimension_id'] ?? 0; $pricing = self::resolveOrderItemInvoicePricing($order_item); - $discount_percentage = $use_itemized_discount + // The customer discount (e.g. kd customer 35131752 with 15% global e-conomic discount) + // is applied at the line level. Combined with per-item discounts using max() so the + // biggest discount wins, and so we never accidentally apply a 15% discount on top of + // an already-discounted per-item price. + $customer_discount_percentage = max(0, min(100, $customer_discount_percentage)); + $itemized_discount_percentage = $use_itemized_discount ? $this->resolveItemizedDiscountPercentageForInvoiceCurrency($pricing) - : 0; + : 0.0; + $discount_percentage = (float)max( + $itemized_discount_percentage, + (float)$customer_discount_percentage + ); // Add the order item to the draft invoice self::addProductLine( (string)$order_item['product']['economic_product_id'], diff --git a/services/nginx/app/objects/collected_order_invoices_o.php b/services/nginx/app/objects/collected_order_invoices_o.php index 479a7057..7027ae18 100644 --- a/services/nginx/app/objects/collected_order_invoices_o.php +++ b/services/nginx/app/objects/collected_order_invoices_o.php @@ -725,6 +725,52 @@ class collected_order_invoices_o extends db return false; } + /** + * Resolve the e-conomic customer discount percentage that should be applied at the + * line level when building the invoice draft. Caches via Redis to avoid hammering + * the e-conomic templates endpoint on every draft sync. + */ + private static function resolveCustomerDiscountPercentageForDraft(int $customer_number): int + { + if ($customer_number <= 0) { + return 0; + } + + $user = (new users_o())->getUserByCustomerNumber($customer_number); + $userId = (int)$user->id; + if ($userId > 0 && defined('redis')) { + try { + $cached = constant('redis')->get_economic_customer_discount_percentage($userId); + if ($cached !== null) { + return max(0, min(100, (int)$cached)); + } + } catch (\Throwable $e) { + // Fall through to the live lookup. + } + } + + try { + $discount = (int)(new \customers\economicCustomers())->getCustomerDiscountPercentage($customer_number); + } catch (\Throwable $e) { + error_log(sprintf( + '[collected_order_invoices_o] Failed to resolve e-conomic customer discount for customer %d: %s', + $customer_number, + $e->getMessage() + )); + return 0; + } + + if ($userId > 0 && defined('redis')) { + try { + constant('redis')->cache_economic_customer_discount_percentage($userId, $discount); + } catch (\Throwable $e) { + // Cache failures are non-fatal. + } + } + + return max(0, min(100, $discount)); + } + /** * Require the invoice draft to not already exist * @throws Exception If the request was not successful @@ -964,7 +1010,18 @@ class collected_order_invoices_o extends db break; } } - $metrics = (new economic())->invoices->draft->add_orders($draft_id, $order_objects, $currency, 500, $use_itemized_discounts); + // Look up the customer-level e-conomic discount (e.g. bug #11 customer 35131752 + // "kd" 15%). This is applied at the line level so the draft invoice carries the + // discount percentage that e-conomic expects for the customer. + $customer_discount_percentage = self::resolveCustomerDiscountPercentageForDraft((int)$this->customer_number->value()); + $metrics = (new economic())->invoices->draft->add_orders( + $draft_id, + $order_objects, + $currency, + 500, + $use_itemized_discounts, + $customer_discount_percentage + ); $this->last_economic_transfer_metrics = [ 'draft_invoice_id' => $draft_id, 'currency' => (string)$currency, diff --git a/services/nginx/app/tests/Unit/Invoicing/CollectedInvoiceEconomicBatchTransferWiringTest.php b/services/nginx/app/tests/Unit/Invoicing/CollectedInvoiceEconomicBatchTransferWiringTest.php index 8b793e49..251630b8 100644 --- a/services/nginx/app/tests/Unit/Invoicing/CollectedInvoiceEconomicBatchTransferWiringTest.php +++ b/services/nginx/app/tests/Unit/Invoicing/CollectedInvoiceEconomicBatchTransferWiringTest.php @@ -14,7 +14,11 @@ it('routes collected invoice draft line uploads through the multi-order batch en $methodBlock = substr($content, (int)$start, (int)$end - (int)$start); expect($methodBlock)->toContain('$order_objects = [];') - ->and($methodBlock)->toContain('$metrics = (new economic())->invoices->draft->add_orders($draft_id, $order_objects, $currency, 500, $use_itemized_discounts);') + // Bug #11 customer 35131752 — the customer discount is threaded through add_orders + // so the line-level discountPercentage is applied to each line item. + ->and($methodBlock)->toContain('$customer_discount_percentage = self::resolveCustomerDiscountPercentageForDraft') + ->and($methodBlock)->toContain('$metrics = (new economic())->invoices->draft->add_orders(') + ->and($methodBlock)->toContain('$customer_discount_percentage') ->and($methodBlock)->toContain('...$metrics') ->and($methodBlock)->not->toContain('self::addInvoiceToDraft($order[\'id\'], true, $draft_id, $currency);'); }); @@ -36,7 +40,7 @@ it('keeps single-order draft uploads as a wrapper around the batch endpoint', fu $batchBlock = substr($content, (int)$singleEnd); expect($batchBlock)->toContain('$draftInvoice->flushLinesInBatches($line_batch_size);') - ->and($batchBlock)->toContain('$draftInvoice->addOrderItemLines($order, $use_itemized_discounts);') + ->and($batchBlock)->toContain('$draftInvoice->addOrderItemLines($order, $use_itemized_discounts, $customer_discount_percentage);') ->and($batchBlock)->toContain("'orders_with_invoice_lines' => \$orders_with_invoice_lines"); }); @@ -50,7 +54,7 @@ it('selects itemized discount mode for collected invoice batch transfers', funct ->and($content)->toContain('invoice_discount_layout') ->and($content)->toContain('hasDiscountedIncludedInvoiceItems') ->and($content)->toContain('orderItemHasBillableDiscount') - ->and($content)->toContain('$metrics = (new economic())->invoices->draft->add_orders($draft_id, $order_objects, $currency, 500, $use_itemized_discounts);'); + ->and($content)->toContain('$customer_discount_percentage'); }); it('includes collected invoice batch transfer metrics in queue results when available', function (): void { diff --git a/services/nginx/app/tests/Unit/Invoicing/EconomicInvoiceDraftCustomerDiscountTest.php b/services/nginx/app/tests/Unit/Invoicing/EconomicInvoiceDraftCustomerDiscountTest.php new file mode 100644 index 00000000..6f8923d7 --- /dev/null +++ b/services/nginx/app/tests/Unit/Invoicing/EconomicInvoiceDraftCustomerDiscountTest.php @@ -0,0 +1,182 @@ +draft_invoice_number = 35131752; + $this->currency = 'DKK'; + $this->conversion_rate = 1.0; + $this->draft_invoice_data = (object)['draftInvoiceNumber' => 35131752]; + } + + protected function sendDraftLines(array $draft_lines): object + { + $this->sentBatches[] = $draft_lines; + return (object)['lines' => $draft_lines]; + } + } +} + +function economic_customer_discount_order_item(float $price, float $product_price, array $overrides = []): array +{ + return array_replace_recursive([ + 'id' => 9001, + 'quantity' => 1, + 'price' => $price, + 'reference' => '', + 'notes' => '', + 'include_in_invoice' => true, + 'product' => [ + 'economic_product_id' => '5', + 'name' => 'Wash', + 'price' => $product_price, + ], + ], $overrides); +} + +it('applies the 15% customer discount to a line item for customer 35131752 "kd"', function (): void { + $draft = new EconomicInvoiceDraftCustomerDiscountProbe(); + + // Order item is at full price (no per-item discount) — exactly the customer 35131752 + // case where the 15% global e-conomic discount was silently dropped. + $draft->addOrderItemLine( + economic_customer_discount_order_item(100.0, 100.0), + [ + 'economic_department_id' => 75, + 'economic_dimension_id' => 1, + ], + false, + false, + 15 + ); + $draft->flushLinesInBatches(); + + $line = $draft->sentBatches[0][0]; + expect($line['product']['productNumber'])->toBe('5') + ->and($line['description'])->toBe('Wash') + ->and($line['quantity'])->toBe(1.0) + ->and($line['unitNetPrice'])->toBe(100.0) + ->and($line['discountPercentage'])->toBe(15.0); +}); + +it('uses the larger discount when both per-item and customer discounts are present', function (): void { + $draft = new EconomicInvoiceDraftCustomerDiscountProbe(); + + // Per-item discount = 10%, customer discount = 15% → max(15, 10) = 15. + $draft->addOrderItemLine( + economic_customer_discount_order_item(90.0, 100.0), + [ + 'economic_department_id' => 75, + 'economic_dimension_id' => 1, + ], + false, + true, + 15 + ); + $draft->flushLinesInBatches(); + + $line = $draft->sentBatches[0][0]; + expect($line['unitNetPrice'])->toBe(100.0) + ->and($line['discountPercentage'])->toBe(15.0); +}); + +it('uses the per-item discount when it is larger than the customer discount', function (): void { + $draft = new EconomicInvoiceDraftCustomerDiscountProbe(); + + // Per-item discount = 25%, customer discount = 15% → max(25, 15) = 25. + $draft->addOrderItemLine( + economic_customer_discount_order_item(75.0, 100.0), + [ + 'economic_department_id' => 75, + 'economic_dimension_id' => 1, + ], + false, + true, + 15 + ); + $draft->flushLinesInBatches(); + + $line = $draft->sentBatches[0][0]; + expect($line['unitNetPrice'])->toBe(100.0) + ->and($line['discountPercentage'])->toBe(25.0); +}); + +it('clamps the customer discount percentage to the 0..100 range', function (): void { + $draft = new EconomicInvoiceDraftCustomerDiscountProbe(); + + $draft->addOrderItemLine( + economic_customer_discount_order_item(100.0, 100.0), + [ + 'economic_department_id' => 75, + 'economic_dimension_id' => 1, + ], + false, + false, + 150 + ); + $draft->flushLinesInBatches(); + + $line = $draft->sentBatches[0][0]; + expect($line['discountPercentage'])->toBe(100.0); +}); + +it('emits no line discount when both per-item and customer discounts are zero', function (): void { + $draft = new EconomicInvoiceDraftCustomerDiscountProbe(); + + $draft->addOrderItemLine( + economic_customer_discount_order_item(100.0, 100.0), + [ + 'economic_department_id' => 75, + 'economic_dimension_id' => 1, + ], + false, + false, + 0 + ); + $draft->flushLinesInBatches(); + + $line = $draft->sentBatches[0][0]; + expect($line['unitNetPrice'])->toBe(100.0) + ->and($line['discountPercentage'])->toBe(0.0); +}); + +it('keeps base behavior unchanged when the customer discount is zero', function (): void { + $draft = new EconomicInvoiceDraftCustomerDiscountProbe(); + + // No customer discount, no per-item discount — unit price should be the final price. + $draft->addOrderItemLine( + economic_customer_discount_order_item(100.0, 100.0), + [ + 'economic_department_id' => 75, + 'economic_dimension_id' => 1, + ], + false, + false, + 0 + ); + $draft->flushLinesInBatches(); + + $line = $draft->sentBatches[0][0]; + expect($line['unitNetPrice'])->toBe(100.0) + ->and($line['discountPercentage'])->toBe(0.0); +}); diff --git a/services/nginx/app/tests/Unit/Invoicing/EconomicInvoiceDraftDiscountLineModeWiringTest.php b/services/nginx/app/tests/Unit/Invoicing/EconomicInvoiceDraftDiscountLineModeWiringTest.php index 14c62a99..d71c04e4 100644 --- a/services/nginx/app/tests/Unit/Invoicing/EconomicInvoiceDraftDiscountLineModeWiringTest.php +++ b/services/nginx/app/tests/Unit/Invoicing/EconomicInvoiceDraftDiscountLineModeWiringTest.php @@ -1,6 +1,6 @@ not->toBeFalse(); @@ -13,7 +13,11 @@ it('only adds TotDiscount aggregate line when itemized discounts are disabled', expect($end)->toBeGreaterThan($start); $block = substr($content, (int)$start, (int)$end - (int)$start); + // The aggregate TotDiscount line is only added when neither itemized discounts + // nor a customer-level e-conomic discount is in effect. The customer discount + // (e.g. bug #11 customer 35131752 "kd" 15%) is applied at the line level instead. expect($block) - ->toContain('if (!$use_itemized_discounts && $total_discount > 0)') + ->toContain('if (!$use_itemized_discounts && $customer_discount_percentage === 0 && $total_discount > 0)') ->toContain('self::addProductDiscountLine($total_discount'); }); +