service->get_selected( $this->contexts->current( 'any' ) ) as list( $extra, $selection ) ) { if ( $extra->id === $extra_id ) { $this->set_item_meta( $item, $extra->id, $extra->type, $selection->value ); return; } } } /** * Classic checkout (`woocommerce_checkout_create_order`). The order is saved by WooCommerce afterwards. * * @param mixed $order Order. */ public function on_classic_order( $order ): void { if ( $order instanceof \WC_Order ) { $this->write( $order, 'checkout_classic' ); } } /** * Checkout block (`woocommerce_store_api_checkout_update_order_meta`). * * @param mixed $order Order. */ public function on_store_api_order( $order ): void { if ( $order instanceof \WC_Order ) { $this->write( $order, 'checkout_block' ); $order->save(); } } /** * Writes the snapshot from the current cart. Idempotent. * * @param \WC_Order $order Order. * @param string $location Context location. * @return array Snapshot. */ public function write( \WC_Order $order, string $location ): array { $cart = function_exists( 'WC' ) ? WC()->cart : null; if ( ! $cart instanceof \WC_Cart ) { return array(); } $context = $this->contexts->from_cart( $cart, $location ); $visible = $this->service->get_visible_extras( $context ); $context = $context->with_offered( $visible ); $fees = $cart->get_fees(); $items = array(); foreach ( $this->service->get_selected( $context ) as list( $extra, $selection ) ) { $fee_key = FeeApplier::FEE_PREFIX . $extra->id; $fee = $fees[ $fee_key ] ?? null; $item = array( 'extra_id' => $extra->id, 'type' => $extra->type, 'label' => $extra->label, 'value' => $selection->value, 'display_value' => $this->service->display_value( $extra, $selection ), 'amount' => $fee ? Money::decimal( (float) $fee->total ) : '0', 'tax' => $fee ? Money::decimal( (float) $fee->tax ) : '0', 'fee_key' => $fee ? $fee_key : null, ); /** * Filters one snapshot item before it is stored in the order. * * @since 1.0.0 * * @param array $item Snapshot item. * @param \Extrova\Core\Extra $extra Extra. * @param \Extrova\Core\Selection $selection Selection. * @param \WC_Order $order Order. */ $item = apply_filters( 'extrova_order_snapshot_item', $item, $extra, $selection, $order ); if ( is_array( $item ) ) { $items[] = $item; } } $snapshot = array( 'schema_version' => 1, 'items' => $items, ); $order->delete_meta_data( self::META_SNAPSHOT ); $order->delete_meta_data( self::META_EXTRA_ID ); if ( array() !== $items ) { $order->update_meta_data( self::META_SNAPSHOT, $snapshot ); foreach ( $items as $item ) { $order->add_meta_data( self::META_EXTRA_ID, (string) $item['extra_id'], false ); } } $this->ensure_fee_item_meta( $order, $items ); /** * Fires after the extras are stored in the order. `$context->offered_extras` holds the * extras that were visible (offered), accepted ones are in the snapshot. * In the Checkout block it may fire several times for the same draft order. * * @since 1.0.0 * * @param \WC_Order $order Order. * @param array $snapshot Snapshot. * @param \Extrova\Core\Context $context Context. */ do_action( 'extrova_order_extras_saved', $order, $snapshot, $context ); return $snapshot; } /** * Fallback: tags fee items that did not get meta through the fee item hook. * * @param \WC_Order $order Order. * @param array $items Snapshot items. */ private function ensure_fee_item_meta( \WC_Order $order, array $items ): void { $by_key = array(); foreach ( $items as $item ) { if ( ! empty( $item['fee_key'] ) ) { $by_key[ (string) $item['fee_key'] ] = $item; } } if ( array() === $by_key ) { return; } foreach ( $order->get_fees() as $fee_item ) { if ( ! $fee_item instanceof \WC_Order_Item_Fee || '' !== (string) $fee_item->get_meta( self::ITEM_META_ID, true ) ) { continue; } $legacy_key = property_exists( $fee_item, 'legacy_fee_key' ) ? (string) $fee_item->legacy_fee_key : ''; if ( isset( $by_key[ $legacy_key ] ) ) { $match = $by_key[ $legacy_key ]; } else { $match = null; foreach ( $by_key as $candidate ) { if ( $fee_item->get_name() === $this->fee_name_for( $candidate ) ) { $match = $candidate; break; } } } if ( $match ) { $this->set_item_meta( $fee_item, (string) $match['extra_id'], (string) $match['type'], $match['value'] ); } } } /** * Fee name of a snapshot item, read from the current cart. * * @param array $item Snapshot item. */ private function fee_name_for( array $item ): string { $fees = WC()->cart ? WC()->cart->get_fees() : array(); $fee = $fees[ (string) $item['fee_key'] ] ?? null; return $fee ? (string) $fee->name : ''; } /** * Writes the fee item meta. * * @param \WC_Order_Item_Fee $item Fee item. * @param string $extra_id Extra id. * @param string $type Type slug. * @param mixed $value Value. */ private function set_item_meta( \WC_Order_Item_Fee $item, string $extra_id, string $type, mixed $value ): void { $item->update_meta_data( self::ITEM_META_ID, $extra_id ); $item->update_meta_data( self::ITEM_META_TYPE, $type ); $item->update_meta_data( self::ITEM_META_VALUE, is_scalar( $value ) ? (string) $value : (string) wp_json_encode( $value ) ); } }