Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
f94a92d
creating promos feed file and adding to FeedManager
carterbuce Mar 8, 2025
ff27a93
pulling lots of functionality out of concrete feed classes and into A…
carterbuce Mar 8, 2025
ec12af5
moving $file_writer into AbstractFeed, adding PromotionsFeedGenerator
carterbuce Mar 8, 2025
a5186bc
add comment
carterbuce Mar 10, 2025
694cc23
add coupon mapping logic
carterbuce Mar 13, 2025
4a1b46d
Merge branch 'main' into promos-feed
carterbuce Mar 13, 2025
0ca45a8
merge with R&R feed changes
carterbuce Mar 13, 2025
2d93ba1
fix lint
carterbuce Mar 13, 2025
7f2eefa
fixes
carterbuce Mar 13, 2025
9c4e9e0
more error logging
carterbuce Mar 13, 2025
851986b
fix file writer formatting
carterbuce Mar 13, 2025
24e3c35
fix file formatting
carterbuce Mar 13, 2025
0178d6e
remove todo
carterbuce Mar 13, 2025
223db9c
merge PromotionsFeedUtils.php into FeedUploadUtils.php, remove error …
carterbuce Mar 13, 2025
22a88d3
add target filter logic
carterbuce Mar 15, 2025
0ba7aa3
add target filter logic for product categories
carterbuce Mar 15, 2025
8d42f1a
add target filter logic for excluded product categories
carterbuce Mar 15, 2025
b96dd7d
finalize logging
carterbuce Mar 15, 2025
7420217
switch to wc_get_products
carterbuce Mar 15, 2025
b4f7ca7
add test
carterbuce Mar 17, 2025
b7903fa
add test
carterbuce Mar 17, 2025
8414e0b
add product category test
carterbuce Mar 17, 2025
e503865
add coupon validation tests
carterbuce Mar 17, 2025
9e25dc3
comments
carterbuce Mar 19, 2025
358c815
Merge branch 'main' into promos-feed
carterbuce Mar 19, 2025
9ad1cfe
comments
carterbuce Mar 19, 2025
ac4be0f
comments
carterbuce Mar 19, 2025
b64fc65
comments, plus adding ordering by ID to wc_get_products calls to fix …
carterbuce Mar 19, 2025
6674dda
comments
carterbuce Mar 19, 2025
0535d17
Merge branch 'main' into promos-feed
carterbuce Mar 20, 2025
c09bf12
adding detail to construct (trying to trigger github phpcs checks)
carterbuce Mar 20, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
115 changes: 78 additions & 37 deletions includes/Feed/AbstractFeed.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
use WooCommerce\Facebook\Framework\Api\Exception;
use WooCommerce\Facebook\Framework\Helper;
use WooCommerce\Facebook\Framework\Plugin\Exception as PluginException;
use WooCommerce\Facebook\Utilities\Heartbeat;

defined( 'ABSPATH' ) || exit;

Expand All @@ -32,20 +33,20 @@ abstract class AbstractFeed {
/** The action slug for triggering file upload */
const FEED_GEN_COMPLETE_ACTION = 'wc_facebook_feed_generation_completed_';

/** Schedule feed generation on some interval hook name for children classes. */
const SCHEDULE_CALL_BACK = 'schedule_feed_generation';
/** Schedule an immediate file generator on the scheduler hook name. For testing mostly. */
const REGENERATE_CALL_BACK = 'regenerate_feed';
/** Make upload call to Meta hook name for children classes. */
const UPLOAD_CALL_BACK = 'send_request_to_upload_feed';
/** Stream file to upload endpoint hook name for children classes. */
const STREAM_CALL_BACK = 'handle_feed_data_request';
/** Hook prefix for Legacy REST API hook name */
const LEGACY_API_PREFIX = 'woocommerce_api_';
/** @var string the WordPress option name where the secret included in the feed URL is stored */
const OPTION_FEED_URL_SECRET = 'wc_facebook_feed_url_secret_';


/**
* The feed writer instance for the given feed.
*
* @var FeedFileWriter
* @since 3.5.0
*/
protected FeedFileWriter $feed_writer;

/**
* The feed generator instance for the given feed.
*
Expand All @@ -63,44 +64,50 @@ abstract class AbstractFeed {
protected AbstractFeedHandler $feed_handler;

/**
* The name of the data feed.
* Initialize feed properties.
*
* @var string
* @param FeedFileWriter $feed_writer The feed file writer instance.
* @param AbstractFeedHandler $feed_handler The feed handler instance.
* @param FeedGenerator $feed_generator The feed generator instance.
*/
protected string $data_stream_name;
protected function init( FeedFileWriter $feed_writer, AbstractFeedHandler $feed_handler, FeedGenerator $feed_generator ): void {
$this->feed_writer = $feed_writer;
$this->feed_handler = $feed_handler;
$this->feed_generator = $feed_generator;

/**
* The option name for the feed URL secret.
*
* @var string
*/
protected string $feed_url_secret_option_name;

/**
* The type of feed as per the endpoint requirements.
*
* @var string
*/
protected string $feed_type;
$this->feed_generator->init();
$this->add_hooks();
}

/**
* The interval in seconds for the feed generation.
* Adds the necessary hooks for feed generation and data request handling.
*
* @var int
* @since 3.5.0
*/
protected int $gen_feed_interval;
protected function add_hooks(): void {
add_action( static::get_feed_gen_scheduling_interval(), array( $this, 'schedule_feed_generation' ) );
add_action( self::GENERATE_FEED_ACTION . static::get_data_stream_name(), array( $this, 'regenerate_feed' ) );
add_action( self::FEED_GEN_COMPLETE_ACTION . static::get_data_stream_name(), array( $this, 'send_request_to_upload_feed' ) );
add_action(
self::LEGACY_API_PREFIX . self::REQUEST_FEED_ACTION . static::get_data_stream_name(),
array(
$this,
'handle_feed_data_request',
)
);
}

/**
* Schedules the recurring feed generation.
*
* @since 3.5.0
*/
public function schedule_feed_generation(): void {
$schedule_action_hook_name = self::GENERATE_FEED_ACTION . $this->data_stream_name;
$schedule_action_hook_name = self::GENERATE_FEED_ACTION . static::get_data_stream_name();
if ( ! as_next_scheduled_action( $schedule_action_hook_name ) ) {
as_schedule_recurring_action(
time(),
$this->gen_feed_interval,
static::get_feed_gen_interval(),
$schedule_action_hook_name,
array(),
facebook_for_woocommerce()->get_id_dasherized()
Expand Down Expand Up @@ -132,10 +139,10 @@ public function regenerate_feed(): void {
* @since 3.5.0
*/
public function send_request_to_upload_feed(): void {
$name = $this->data_stream_name;
$name = static::get_data_stream_name();
$data = array(
'url' => self::get_feed_data_url(),
'feed_type' => $this->feed_type,
'feed_type' => static::get_feed_type(),
'update_type' => 'CREATE',
);

Expand All @@ -160,7 +167,7 @@ public function send_request_to_upload_feed(): void {
*/
public function get_feed_data_url(): string {
$query_args = array(
'wc-api' => self::REQUEST_FEED_ACTION . $this->data_stream_name,
'wc-api' => self::REQUEST_FEED_ACTION . static::get_data_stream_name(),
'secret' => self::get_feed_secret(),
);

Expand All @@ -177,10 +184,12 @@ public function get_feed_data_url(): string {
* @since 3.5.0
*/
public function get_feed_secret(): string {
$secret = get_option( $this->feed_url_secret_option_name, '' );
$secret_option_name = self::OPTION_FEED_URL_SECRET . static::get_data_stream_name();

$secret = get_option( $secret_option_name, '' );
if ( ! $secret ) {
$secret = wp_hash( 'example-feed-' . time() );
update_option( $this->feed_url_secret_option_name, $secret );
update_option( $secret_option_name, $secret );
}

return $secret;
Expand All @@ -196,12 +205,12 @@ public function get_feed_secret(): string {
* @since 3.5.0
*/
public function handle_feed_data_request(): void {
$name = $this->data_stream_name;
$name = static::get_data_stream_name();
\WC_Facebookcommerce_Utils::log( "{$name} feed: Meta is requesting feed file." );

$file_path = $this->feed_handler->get_feed_writer()->get_file_path();
$file_path = $this->feed_writer->get_file_path();

// regenerate if the file doesn't exist.
// regenerate if the file doesn't exist using the legacy flow.
if ( ! file_exists( $file_path ) ) {
$this->feed_handler->generate_feed_file();
}
Expand Down Expand Up @@ -249,4 +258,36 @@ public function handle_feed_data_request(): void {
}
exit;
}

/**
* Get the data stream name for the given feed.
*
* @return string
*/
abstract protected static function get_data_stream_name(): string;

/**
* Get the data feed type.
*
* @return string
*/
abstract protected static function get_feed_type(): string;

/**
* Get the feed generation interval. Must be longer than the heartbeat.
*
* @return int
*/
protected static function get_feed_gen_interval(): int {
return DAY_IN_SECONDS;
}

/**
* Get the Heartbeat interval to ensure that feed gen is scheduled. Must be shorter than the feed gen interval.
*
* @return string Heartbeat constant value
*/
protected static function get_feed_gen_scheduling_interval(): string {
return Heartbeat::HOURLY;
}
}
13 changes: 12 additions & 1 deletion includes/Feed/CsvFeedFileWriter.php
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,10 @@ public function write_temp_feed_file( array $data ): void {
foreach ( $data as $obj ) {
$row = [];
foreach ( $accessors as $accessor ) {
$row[] = $obj[ $accessor ] ?? '';
// Map each field in the row to ensure proper string conversion
$value = $obj[ $accessor ] ?? '';
$row[] = $this->format_field( $value );

}
if ( fputcsv( $temp_feed_file, $row, $this->delimiter, $this->enclosure, $this->escape_char ) === false ) {
throw new PluginException( 'Failed to write a CSV data row.', 500 );
Expand All @@ -174,6 +177,14 @@ public function write_temp_feed_file( array $data ): void {
fclose( $temp_feed_file );
}

protected function format_field( $value ) {
if ( is_array( $value ) || is_object( $value ) ) {
return wp_json_encode( $value );
}
return $value;
}


/**
* Creates files in the feed directory to prevent directory listing and hotlinking.
*
Expand Down
7 changes: 4 additions & 3 deletions includes/Feed/FeedManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
* @since 3.5.0
*/
class FeedManager {
const PROMOTIONS = 'promotions';
const RATINGS_AND_REVIEWS = 'ratings_and_reviews';

/**
Expand Down Expand Up @@ -45,14 +46,14 @@ public function __construct() {
*
* @param string $data_stream_name The name of the data stream.
*
* phpcs:ignore -- Method to be implemented when new feed types are added.
*
* @return AbstractFeed The created feed instance derived from AbstractFeed.
* @throws \InvalidArgumentException If the data stream doesn't correspond to a FeedType.
* @since 3.5.0
*/
private function create_feed( string $data_stream_name ): AbstractFeed {
switch ( $data_stream_name ) {
case self::PROMOTIONS:
return new PromotionsFeed();
case self::RATINGS_AND_REVIEWS:
return new RatingsAndReviewsFeed();
default:
Expand All @@ -67,7 +68,7 @@ private function create_feed( string $data_stream_name ): AbstractFeed {
* @since 3.5.0
*/
public static function get_active_feed_types(): array {
return array( self::RATINGS_AND_REVIEWS );
return array( self::PROMOTIONS, self::RATINGS_AND_REVIEWS );
}

/**
Expand Down
Loading