diff --git a/services/nginx/app/modules/attachments/helpers/attachment_content.php b/services/nginx/app/modules/attachments/helpers/attachment_content.php index b395fd8d..cbf410f2 100644 --- a/services/nginx/app/modules/attachments/helpers/attachment_content.php +++ b/services/nginx/app/modules/attachments/helpers/attachment_content.php @@ -5,6 +5,7 @@ namespace attachments\helpers; class attachment_content { const OTHER_TYPE_WASH_CERTIFICATE = 'WASH_CERTIFICATE'; + const OTHER_TYPE_SELF_SERVE_WASH = 'SELF_SERVE_WASH'; public ?string $image; // Used to store the attachment object name, in the attachment store. public ?string $document; // Used to store the attachment object name, in the attachment store. public ?attachment_relation $relation; // Used to store the attachment relation object. @@ -49,4 +50,4 @@ class attachment_content $this->relation = $relation; return $this; } -} \ No newline at end of file +} diff --git a/services/nginx/app/modules/selfserve/classes/selfserve_lane_command_arguments.php b/services/nginx/app/modules/selfserve/classes/selfserve_lane_command_arguments.php index ef49d7d7..dd8510f6 100644 --- a/services/nginx/app/modules/selfserve/classes/selfserve_lane_command_arguments.php +++ b/services/nginx/app/modules/selfserve/classes/selfserve_lane_command_arguments.php @@ -6,6 +6,7 @@ class selfserve_lane_command_arguments { public ?string $license_plate = null; public ?int $customer_number = null; + public ?int $subuser_id = null; public bool $defer_relay_side_effects = false; /** @@ -25,6 +26,12 @@ class selfserve_lane_command_arguments return $this; } + public function setSubuserId(?int $subuser_id): self + { + $this->subuser_id = $subuser_id !== null && $subuser_id > 0 ? $subuser_id : null; + return $this; + } + public function setDeferRelaySideEffects(bool $defer_relay_side_effects): self { $this->defer_relay_side_effects = $defer_relay_side_effects; @@ -40,6 +47,9 @@ class selfserve_lane_command_arguments if (array_key_exists('customer_number', $params)) { $this->setCustomerNumber($params['customer_number']); } + if (array_key_exists('subuser_id', $params)) { + $this->setSubuserId($params['subuser_id'] === null ? null : (int)$params['subuser_id']); + } if (array_key_exists('defer_relay_side_effects', $params)) { $this->setDeferRelaySideEffects(filter_var( $params['defer_relay_side_effects'], diff --git a/services/nginx/app/modules/selfserve/traits/selfserve_lane_invoice_t.php b/services/nginx/app/modules/selfserve/traits/selfserve_lane_invoice_t.php index 76258df3..bb87a366 100644 --- a/services/nginx/app/modules/selfserve/traits/selfserve_lane_invoice_t.php +++ b/services/nginx/app/modules/selfserve/traits/selfserve_lane_invoice_t.php @@ -6,13 +6,18 @@ require_once WD . '/modules/selfserve/helpers/selfserve_lane_mode.php'; require_once WD . '/modules/selfserve/classes/selfserve_lane.php'; use classes\selfserve; +use classes\economic; use Exception; +use attachments\helpers\attachment_content; +use modules\selfserve\classes\selfserve_lane_command_arguments; use modules\selfserve\classes\selfserve_lane; use modules\selfserve\helpers\selfserve_lane_mode; use modules\selfserve\helpers\selfserve_lane_status; use objects\customer_vehicles_o; use objects\order_items_o; use objects\orders_o; +use objects\selfserve_wash_sessions_o; +use objects\subusers_o; trait selfserve_lane_invoice_t { @@ -102,7 +107,7 @@ trait selfserve_lane_invoice_t * @return bool True on success, false on failure * @throws Exception if lane ID is not set, lane is not occupied, customer number or license plate is not set, or product ID is not set */ - public function invoice(): bool + public function invoice(?selfserve_lane_command_arguments $arguments = null): bool { $this->last_invoice_order_id = null; @@ -115,12 +120,10 @@ trait selfserve_lane_invoice_t $included_minutes = $this->resolveIncludedMinutesForBilling(); $billable_minutes = $this->calculateBillableMinutes($elapsed_minutes, $included_minutes); - if ($billable_minutes <= 0) { - return true; + $order = $this->createInvoiceOrderContext($arguments); + if ($billable_minutes > 0) { + $this->billable_minutes_order_item = $this->addMinuteBillingLine((int)$order->id, (int)$product_id, $billable_minutes); } - - $order = $this->createInvoiceOrderContext(); - $this->billable_minutes_order_item = $this->addMinuteBillingLine((int)$order->id, (int)$product_id, $billable_minutes); return true; } @@ -212,10 +215,13 @@ trait selfserve_lane_invoice_t return max(0, $elapsed_minutes - $included_minutes); } - protected function createInvoiceOrderContext(): orders_o + protected function createInvoiceOrderContext(?selfserve_lane_command_arguments $arguments = null): orders_o { + $billing_customer_number = $this->getCustomerNumber(); + $draft_customer_number = (new economic())->getTransactionDraftCustomerNumber(); + $order_customer_number = $draft_customer_number ?? $billing_customer_number; $order = (new orders_o())->add( - $this->getCustomerNumber(), + $order_customer_number, self::INVOICE_SYSTEM_USER_ID, '', '', @@ -224,10 +230,101 @@ trait selfserve_lane_invoice_t ); $order->lane->set($this->id); $this->last_invoice_order_id = (int)$order->id; + $this->attachSelfServeMetadataToOrder($order, $billing_customer_number, $draft_customer_number, $arguments); return $order; } + protected function attachSelfServeMetadataToOrder( + orders_o $order, + int $billing_customer_number, + ?int $draft_customer_number, + ?selfserve_lane_command_arguments $arguments = null + ): void { + try { + $order->addAttachment( + (new attachment_content())->setOther( + $this->buildSelfServeOrderAttachmentPayload($billing_customer_number, $draft_customer_number, $arguments) + ) + ); + } catch (\Throwable) { + // Metadata attachments must not block billing; the order itself is the source of record. + } + } + + protected function buildSelfServeOrderAttachmentPayload( + int $billing_customer_number, + ?int $draft_customer_number, + ?selfserve_lane_command_arguments $arguments = null + ): array { + $session = $this->findOpenSelfServeSessionForAttachment($billing_customer_number); + $subuser_id = $arguments?->subuser_id; + + return [ + 'type' => attachment_content::OTHER_TYPE_SELF_SERVE_WASH, + 'source' => 'selfserve', + 'customer_number' => $billing_customer_number, + 'draft_customer_number' => $draft_customer_number, + 'subuser_id' => $subuser_id, + 'subuser' => $this->formatSelfServeAttachmentSubuser($subuser_id), + 'session_id' => $session?->id, + 'lane_id' => (int)$this->id, + 'department_id' => (int)$this->department_lane->department->value(), + 'license_plate' => (string)$this->getLicensePlate(), + 'lane_status' => $this->getLaneStatus()->name, + 'lane_mode' => $this->getLaneMode()->name, + 'wash_start_time' => (int)$this->getWashStartTime(), + 'elapsed_wash_time_seconds' => (int)$this->getElapsedWashTime(), + 'created_at' => date('Y-m-d H:i:s'), + ]; + } + + protected function findOpenSelfServeSessionForAttachment(int $billing_customer_number): ?selfserve_wash_sessions_o + { + $license_plate = trim((string)$this->getLicensePlate()); + if ($license_plate === '') { + return null; + } + + try { + $session = (new selfserve_wash_sessions_o())->selectLatestOpenByLaneAndReg( + (int)$this->id, + selfserve::standardize_registration($license_plate), + $billing_customer_number > 0 ? $billing_customer_number : null + ); + return $session->exists() ? $session : null; + } catch (\Throwable) { + return null; + } + } + + protected function formatSelfServeAttachmentSubuser(?int $subuser_id): ?array + { + if ($subuser_id === null || $subuser_id <= 0) { + return null; + } + + try { + $subuser = (new subusers_o())->select($subuser_id); + if (!$subuser->exists()) { + return [ + 'id' => $subuser_id, + ]; + } + + return [ + 'id' => (int)$subuser->id, + 'name' => $subuser->name->value(), + 'username' => $subuser->username->value(), + 'email' => $subuser->email->value(), + ]; + } catch (\Throwable) { + return [ + 'id' => $subuser_id, + ]; + } + } + protected function addMinuteBillingLine(int $order_id, int $product_id, int $quantity): order_items_o { return (new order_items_o())->addItemToOrder( diff --git a/services/nginx/app/routes/moduleSelfServeRoute.php b/services/nginx/app/routes/moduleSelfServeRoute.php index b77aad7d..359b31c1 100644 --- a/services/nginx/app/routes/moduleSelfServeRoute.php +++ b/services/nginx/app/routes/moduleSelfServeRoute.php @@ -519,10 +519,12 @@ class moduleSelfServeRoute // Execute the command try { $this->applyShellyTransportOverride($lane); + $subuser = (new authentication())->get_subuser(); $args = new \modules\selfserve\classes\selfserve_lane_command_arguments(); $args->setParameters([ ...$this->getParametersAsArray(), // Pass all parameters 'customer_number' => $customer_number, // Get customer number from request user + 'subuser_id' => $subuser === false ? null : (int)$subuser->id, ]); $lane->execute($command, $args); $response->success([ diff --git a/services/nginx/app/tests/Api/OrderItemsApiTest.php b/services/nginx/app/tests/Api/OrderItemsApiTest.php index d3e0e6ac..577c80f6 100644 --- a/services/nginx/app/tests/Api/OrderItemsApiTest.php +++ b/services/nginx/app/tests/Api/OrderItemsApiTest.php @@ -100,7 +100,7 @@ it('returns the extraordinary chemistry product with requires_note enabled', fun 'price' => 299, 'requires_note' => 0, ]); - $session = api_fixtures()->createUserSession(['list_products']); + $session = api_fixtures()->createUserSession([], ['group_id' => 1]); $response = api_client()->get('/products?id=' . $product['id'], $session['headers']);