cart = $cart; $this->products = $products; $this->money = $money; $this->discounts = $discounts; $this->discountComposer = $discountComposer; } /** * Unique component identifier. * * @return string */ public function slug(): string { return 'cart-summary'; } /** * Machine-readable attribute definitions. * * @return array> */ public function schema(): array { return array( 'show_actions' => array( 'type' => 'boolean', 'default' => true, 'label' => __( 'Show cart actions', 'releasecart' ), 'description' => __( 'Show remove and quantity-update controls for each line.', 'releasecart' ), 'control' => 'toggle', ), 'checkout_page' => array( 'type' => 'integer', 'default' => 0, 'label' => __( 'Checkout page', 'releasecart' ), 'description' => __( 'The page the checkout link points to. Leave at the default to use the site-wide checkout page setting.', 'releasecart' ), 'control' => 'page_select', 'zero_label' => __( 'Default (site checkout page)', 'releasecart' ), ), 'title' => array( 'type' => 'string', 'default' => __( 'Your cart', 'releasecart' ), 'label' => __( 'Title', 'releasecart' ), 'description' => __( 'Heading shown above the cart. Leave empty to hide the heading.', 'releasecart' ), 'control' => 'text', ), 'show_plan_details' => array( 'type' => 'boolean', 'default' => true, 'label' => __( 'Show plan details', 'releasecart' ), 'description' => __( "Show each line's license length and activation limit under the product name.", 'releasecart' ), 'control' => 'toggle', ), ); } /** * Render the cart summary. * * @param array $attrs Resolved attribute values. * @param RenderContext $ctx Per-request customer/license state. * @return string Escaped HTML. */ public function render( array $attrs, RenderContext $ctx ): string { $cartState = $this->cart->getCart(); // Use an in-memory sample cart only in an editor preview. $isSample = $cartState->isEmpty() && $ctx->isEditorPreview(); if ( $isSample ) { $cartState = SamplePreview::cart(); } $showPlanDetails = (bool) ( $attrs['show_plan_details'] ?? true ); $notices = array(); $lines = $this->lineData( $cartState, $showPlanDetails, $notices ); $total = 0; foreach ( $lines as $line ) { $total += (int) $line['subtotal_minor']; } // Price the stored code against snapshot amounts without changing the cart. $discountItems = array(); foreach ( $lines as $line ) { $discountItems[] = array( 'product_id' => (int) ( $line['product_id'] ?? 0 ), 'subtotal' => (int) ( $line['subtotal_minor'] ?? 0 ), ); } $discount = StoredDiscountPreview::view( $this->discounts, $cartState->discountCode(), $discountItems, $this->money, $this->discountComposer ); if ( 'invalid' === $discount['state'] && '' !== $discount['error'] ) { $notices[] = $discount['error']; } $data = array( 'title' => (string) $attrs['title'], 'show_actions' => (bool) $attrs['show_actions'], 'show_plan_details' => $showPlanDetails, 'is_empty' => $cartState->isEmpty(), 'lines' => $lines, 'discount' => $discount, // Use the same combined reduction as the order layer. 'subtotal' => $this->money->format( $total - $discount['total_minor'] ), 'checkout_url' => $this->resolveCheckoutUrl( (int) $attrs['checkout_page'] ), 'notices' => $notices, 'total_label' => $this->totalLabel(), ); return $isSample ? $this->renderSample( $data ) : $this->renderTemplate( $data ); } /** * The label of the summary's last row. * * @return string The label, stripped of tags; "Total" when a listener returns nothing usable. */ private function totalLabel(): string { $default = __( 'Total', 'releasecart' ); /** * Filters the label of the cart summary's last row. * * A store can rename it, for example to "Total due". Tags are stripped; * an empty or non-string return keeps "Total". * * @since 1.0.0 * * @param string $label The label. Default "Total". */ $label = apply_filters( 'releasecart_cart_summary_total_label', $default ); $label = is_string( $label ) ? wp_strip_all_tags( $label ) : ''; return '' === $label ? $default : $label; } /** * Build the template-ready view data for every line in a cart. * * Clamp stale quantities with the tier rule and retain snapshot amounts. * * @param Cart $cartState The cart to describe. * @param bool $showPlanDetails Whether to attach the tier meta lines. * @param array|null $notices Out parameter: one advisory per line * whose quantity the tier rule adjusted. * @return array> One row per line. */ private function lineData( Cart $cartState, bool $showPlanDetails, ?array &$notices = null ): array { $notices = array(); $rows = array(); $flags = TierMetaPresenter::flagsFrom( array() ); foreach ( $cartState->lines() as $line ) { $price = $this->livePrice( $line->productId(), $line->priceId() ); $requested = $line->quantity(); $quantity = null !== $price ? $price->clampQuantity( $requested ) : $requested; if ( null !== $price && $quantity !== $requested ) { $notices[] = OrderService::quantityAdjustedNotice( $line->productName(), $quantity ); } $subtotal = $line->unitAmount() * $quantity; // A missing tier has no plan details. $meta = ( $showPlanDetails && null !== $price ) ? TierMetaPresenter::lines( $price, $flags, $this->isLicensed( $line->productId() ) ) : array(); $rows[] = array( 'product_id' => $line->productId(), 'price_id' => $line->priceId(), 'product_name' => $line->productName(), 'price_name' => $line->priceName(), 'quantity' => $quantity, 'unit_amount' => $this->money->format( $line->unitAmount() ), 'subtotal' => $this->money->format( $subtotal ), 'subtotal_minor' => $subtotal, 'min' => null !== $price ? $price->minQuantity() : 1, 'max' => null !== $price ? $price->maxQuantity() : null, 'fixed_single' => null !== $price ? $price->isFixedSingle() : false, 'meta' => $meta, ); } return $rows; } /** * Resolve the live tier for a cart line. * * @param int $productId The line's product id. * @param int $priceId The line's price id. * @return ProductPrice|null The active tier, or null. */ private function livePrice( int $productId, int $priceId ): ?ProductPrice { $product = $this->products->get( $productId ); return null === $product ? null : $product->activePrice( $priceId ); } /** * Whether the product issues licenses, so its license facts apply. * * @param int $productId The cart line's product id. * @return bool */ private function isLicensed( int $productId ): bool { $product = $this->products->get( $productId ); return null !== $product && $product->isLicensed(); } /** * Resolve the selected checkout page or the site default. * * @param int $checkoutPageAttr The `checkout_page` attribute value. * @return string|null The checkout permalink, or null when nothing resolves. */ private function resolveCheckoutUrl( int $checkoutPageAttr ): ?string { $pageId = $checkoutPageAttr; if ( $pageId <= 0 ) { $settings = get_option( 'releasecart_settings' ); if ( is_array( $settings ) && isset( $settings['pages']['checkout'] ) ) { $pageId = (int) $settings['pages']['checkout']; } } if ( $pageId <= 0 ) { return null; } $permalink = get_permalink( $pageId ); return ( is_string( $permalink ) && '' !== $permalink ) ? $permalink : null; } }