forecasts = $forecasts; $this->settings = $settings; $this->supplier_products = $supplier_products; $this->resolver = $resolver; $this->po_service = $po_service; } /** * The reorder list. * * @param array $args Filters (severity, supplier). * @return array{items:array>,counts:array,meta:array} */ public function list( array $args ): array { $severities = ! empty( $args['severity'] ) ? array( (string) $args['severity'] ) : array( 'critical', 'warning', 'recommended' ); $rows = $this->forecasts->by_severity( $severities ); $supplier_filter = (int) ( $args['supplier'] ?? 0 ); $items = array(); foreach ( $rows as $f ) { $item = $this->enrich( $f ); if ( $supplier_filter > 0 && $item['supplier_id'] !== $supplier_filter ) { continue; } $items[] = $item; } return array( 'items' => $items, 'counts' => $this->forecasts->severity_counts(), 'meta' => array( 'configured_products' => $this->settings->count_enabled(), 'forecasted_products' => $this->forecasts->count_all(), 'products_with_velocity' => $this->forecasts->count_with_velocity(), 'severity_filter' => (string) ( $args['severity'] ?? '' ), ), ); } /** * Create draft purchase orders from selected reorder lines, grouped by supplier. * * @param array $selections Selections. * @return array{created:array>,skipped:int[]}|WP_Error */ public function create_pos( array $selections ) { if ( empty( $selections ) ) { return new WP_Error( 'profit_engine_reorder_empty', __( 'Nothing selected.', 'profit-engine' ), array( 'status' => 400 ) ); } $groups = array(); $skipped = array(); foreach ( $selections as $sel ) { $product_id = (int) ( $sel['product_id'] ?? 0 ); $qty = (float) ( $sel['qty'] ?? 0 ); if ( $product_id <= 0 || $qty <= 0 ) { continue; } $primary = $this->supplier_products->primary_for_product( $product_id ); if ( ! $primary ) { $skipped[] = $product_id; continue; } $supplier_id = (int) $primary['supplier_id']; $groups[ $supplier_id ][] = array( 'product_id' => $product_id, 'supplier_sku' => (string) $primary['supplier_sku'], 'qty_ordered' => $qty, 'unit_cost' => Money::to_float( $primary['purchase_cost'] ), ); } $created = array(); foreach ( $groups as $supplier_id => $items ) { $po = $this->po_service->create( array( 'supplier_id' => $supplier_id, 'items' => $items, ) ); if ( ! is_wp_error( $po ) ) { $created[] = array( 'id' => $po['id'], 'po_number' => $po['po_number'], 'supplier_name' => $po['supplier_name'], 'item_count' => count( $po['items'] ), 'grand_total' => $po['grand_total'], ); } } return array( 'created' => $created, 'skipped' => array_values( array_unique( $skipped ) ), ); } /** * Enrich a forecast row with product/supplier/cost info for the UI. * * @param array $f Forecast row. * @return array */ private function enrich( array $f ): array { $pid = (int) $f['product_id']; $primary = $this->supplier_products->primary_for_product( $pid ); $settings = $this->settings->get( $pid ); $unit_cost = 0.0; if ( $primary ) { $unit_cost = Money::to_float( $primary['purchase_cost'] ); } elseif ( $settings ) { $unit_cost = Money::to_float( $settings['current_cost'] ); } $suggested = (int) $f['suggested_qty']; return array( 'product_id' => $pid, 'label' => $this->resolver->unit_label( $pid ), 'sku' => $this->resolver->sku( $pid ), 'severity' => (string) $f['severity'], 'confidence' => (string) $f['confidence'], 'current_stock' => Money::to_float( $f['current_stock'] ), 'avg_daily_sales' => Money::to_float( $f['avg_daily_sales'] ), 'days_remaining' => Money::to_float( $f['days_remaining'] ), 'qty_on_open_pos' => Money::to_float( $f['qty_on_open_pos'] ), 'suggested_qty' => $suggested, 'unit_cost' => $unit_cost, 'estimated_cost' => $suggested * $unit_cost, 'supplier_id' => $primary ? (int) $primary['supplier_id'] : 0, 'supplier_name' => $primary ? (string) $primary['supplier_name'] : '', ); } }