';
}
}
private function callouts( \WC_Product $product, Reference $reference ): string {
$html = '';
if ( Perishable::applies( $product ) ) {
return self::callout( 'info', 'i', __( 'Marked as perishable: no lowest price is required while it is reduced because it is about to spoil or expire (§ 11 Abs. 4 PAngV). The shop shows the reason instead.', 'strikewise' ) );
}
if ( ReferenceService::launched_reduced( $product, $reference ) ) {
$html .= self::callout( 'critical', '!', __( 'Not a price reduction: the product has been reduced since it was added and was never offered at a higher price. CJEU mode does not show it as a reduction.', 'strikewise' ) );
} elseif ( Reference::STATUS_NO_HISTORY === $reference->status ) {
$html .= self::callout(
'warning',
'!',
CjeuMode::is_enabled() && CjeuMode::hides_unknown()
? __( 'No price from before the reduction is known, so the shop does not show it as a reduction. If you know the lowest price of the 30 days before the reduction from your own records, enter it here.', 'strikewise' )
: __( 'No price from before the reduction is known. A strike-through price without the earlier price is not allowed. Enter the earlier price here if you know it.', 'strikewise' )
);
$html .= $this->prior_price_field( $product );
}
if ( $reference->has_price() && null !== $reference->t0 && ! $reference->complete ) {
$html .= self::callout( 'info', 'i', __( 'The recorded history does not cover the full 30 days yet. The lowest known price is used.', 'strikewise' ) );
}
$regular = $product->get_regular_price( 'edit' );
$snapshot = PriceResolver::snapshot( $product );
$cjeu = CjeuMode::is_enabled();
if ( $reference->has_price() && null !== $reference->t0 && null !== $snapshot ) {
$lowest = PriceChart::price( (float) $reference->price );
if ( (float) $reference->price - $snapshot['price'] <= 0.005 ) {
// Not below the lowest price of the 30 days before: not a reduction at all.
$html .= $cjeu
? self::callout(
'ok',
'✓',
/* translators: %s: lowest price */
sprintf( __( 'The sale price is not below the lowest price of the 30 days before the reduction (%s). CJEU mode does not show it as a reduction in the shop.', 'strikewise' ), $lowest )
)
: self::callout(
'critical',
'!',
/* translators: %s: lowest price */
sprintf( __( 'Not a price reduction: the sale price is not below the lowest price of the 30 days before the reduction (%s). Lower the sale price or remove it.', 'strikewise' ), $lowest )
);
} elseif ( '' !== $regular && abs( PriceResolver::to_gross( $product, (float) $regular ) - (float) $reference->price ) >= 0.005 ) {
$strike = PriceChart::price( PriceResolver::to_gross( $product, (float) $regular ) );
$html .= $cjeu
? self::callout(
'ok',
'✓',
/* translators: 1: lowest price, 2: regular price */
sprintf( __( 'CJEU mode shows %1$s as strike-through price in the shop instead of the regular price (%2$s).', 'strikewise' ), $lowest, $strike )
)
: self::callout(
'warning',
'!',
sprintf(
/* translators: 1: regular price, 2: reference price */
__( 'The strike-through price (%1$s) differs from the lowest price of the 30 days before the reduction (%2$s). According to the CJEU (C-330/23), a percentage or strike-through price must refer to the lowest price. Set the regular price to it or turn on CJEU mode.', 'strikewise' ),
$strike,
$lowest
)
);
}
}
/**
* Filters the hints shown in the price history box (the premium UVP mode replaces them).
*
* @param string $html
* @param \WC_Product $product
* @param Reference $reference
*/
return (string) apply_filters( 'strikewise_box_callouts', $html, $product, $reference );
}
/**
* @param array> $rows Newest first.
*/
private function render_history( array $rows, bool $variable ): void {
// Simple products: figures, chart and hints say what matters, the full log is one click away.
if ( ! $variable ) {
echo '';
/* translators: %d: number of entries */
echo esc_html( sprintf( _n( 'Recorded prices (%d entry)', 'Recorded prices (%d entries)', count( $rows ), 'strikewise' ), count( $rows ) ) );
echo '';
$this->history_table( $rows, $variable );
echo '';
return;
}
echo '
';
}
/**
* Input for the earlier price; saved with the product.
*/
private function prior_price_field( \WC_Product $product ): string {
if ( $product->is_type( 'variation' ) || ! current_user_can( 'edit_product', $product->get_id() ) ) {
return '';
}
$label = wc_prices_include_tax() || ! wc_tax_enabled() ? __( 'Earlier price', 'strikewise' ) : __( 'Earlier price (excl. VAT, as your prices are entered)', 'strikewise' );
return '
'
. ' '
. '' . esc_html__( 'Recorded as the price of the 30 days before the reduction, with your name. Only enter a price you can prove.', 'strikewise' ) . '
';
}
/**
* Records the earlier price entered in the box as the price of the 30 days
* before the reduction, if nothing is known about that time yet.
*
* @param \WC_Product $product
*/
public function save_prior_price( $product ): void {
// phpcs:disable WordPress.Security.NonceVerification -- WooCommerce verified the product form nonce.
if ( ! $product instanceof \WC_Product || empty( $_POST[ self::FIELD_PRIOR ] ) || ! $product->get_id() ) {
return;
}
$amount = (float) wc_format_decimal( sanitize_text_field( wp_unslash( $_POST[ self::FIELD_PRIOR ] ) ) );
// phpcs:enable
if ( $amount <= 0 || ! current_user_can( 'edit_product', $product->get_id() ) ) {
return;
}
$currency = PriceResolver::currency();
$open = $this->repository->get_open( $product->get_id(), $currency );
if ( null === $open || ! $open['is_reduced'] ) {
return;
}
$t0 = Repository::to_timestamp( $open['valid_from'] );
if ( $this->repository->earliest_start( $product->get_id(), $currency ) < $t0 ) {
return; // Something is known about the time before the reduction already.
}
$change = new Change();
$change->product_id = $product->get_id();
$change->parent_id = $product->get_parent_id();
$change->currency = $currency;
$change->price = PriceResolver::to_gross( $product, $amount );
$change->regular_price = $change->price;
$change->reduced = false;
$change->valid_from = Repository::to_datetime( $t0 - Settings::window_days() * DAY_IN_SECONDS );
$change->source = 'manual';
$change->user_id = get_current_user_id();
$this->repository->insert_interval( $change, Repository::to_datetime( $t0 ) );
$this->references->refresh( $product );
}
private static function source_label( string $source ): string {
$labels = array(
'editor' => __( 'Editor', 'strikewise' ),
'rest' => __( 'REST API', 'strikewise' ),
'cli' => __( 'WP-CLI', 'strikewise' ),
'schedule' => __( 'Scheduled sale', 'strikewise' ),
'cron' => __( 'Cron', 'strikewise' ),
'sync' => __( 'Sync', 'strikewise' ),
'seed' => __( 'Start', 'strikewise' ),
'lazy' => __( 'Storefront', 'strikewise' ),
'migration' => __( 'Omnibus import', 'strikewise' ),
'manual' => __( 'Entered manually', 'strikewise' ),
);
return isset( $labels[ $source ] ) ? $labels[ $source ] : $source;
}
}