Files
2026-04-28 15:13:50 +02:00

211 lines
7.3 KiB
PHP

<?php
/**
* Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved
*
* This source code is licensed under the license found in the
* LICENSE file in the root directory of this source tree.
*
* @package MetaCommerce
*/
namespace WooCommerce\Facebook\OfferManagement;
use WC_Coupon;
use WP_REST_Request;
use WP_REST_Response;
use WooCommerce\Facebook\FBSignedData\JWTExpiredException;
use WooCommerce\Facebook\RolloutSwitches;
/**
* Base endpoint which offer management endpoints extend.
*/
abstract class OfferManagementEndpointBase {
/**
* Error types defined by Facebook spec
*/
const ERROR_CATALOG_ID_MISMATCH = 'CATALOG_ID_MISMATCH';
const ERROR_JWT_DECODE_FAILURE = 'JWT_DECODE_FAILURE';
const ERROR_JWT_EXPIRED = 'JWT_EXPIRED';
const ERROR_JWT_NOT_FOUND = 'JWT_NOT_FOUND';
const ERROR_OFFER_CODE_ALREADY_EXISTS = 'OFFER_CODE_ALREADY_EXISTS';
const ERROR_OFFER_CONFIGURATION_NOT_SUPPORTED = 'OFFER_CONFIGURATION_NOT_SUPPORTED';
const ERROR_OFFER_CREATE_FAILURE = 'OFFER_CREATE_FAILURE';
const ERROR_OFFER_DELETE_FAILURE = 'OFFER_DELETE_FAILURE';
const ERROR_OFFER_MANAGEMENT_DISABLED = 'OFFER_MANAGEMENT_DISABLED';
const ERROR_OFFER_MANAGEMENT_ERROR = 'OFFER_MANAGEMENT_ERROR';
const ERROR_OFFER_NOT_FOUND = 'OFFER_NOT_FOUND';
const API_NAMESPACE = 'fb_api';
const ROUTE = 'offers';
const IS_FACEBOOK_MANAGED_METADATA_KEY = 'wc_facebook_is_facebook_managed';
const OFFER_TAGS = 'wc_facebook_offer_tags';
const HTTP_OK = 200;
const HTTP_BAD_REQUEST = 400;
const HTTP_UNAUTHORIZED = 401;
const HTTP_FORBIDDEN = 403;
const HTTP_ERROR = 500;
/**
* A list of errors encountered while executing the request
*
* @var array
*/
private array $errors;
public function __construct() {
$this->errors = [];
}
protected function add_error( array $error ): void {
$this->errors[] = $error;
}
protected function add_errors( array $errors ): void {
$this->errors = array_merge( $this->errors, $errors );
}
protected function get_errors(): array {
return $this->errors;
}
final public static function register_endpoints(): void {
CreateOffersEndpoint::register_endpoint();
GetOffersEndpoint::register_endpoint();
DeleteOffersEndpoint::register_endpoint();
}
private static function register_endpoint() {
add_action(
'rest_api_init',
function () {
register_rest_route(
self::API_NAMESPACE,
self::ROUTE,
[
'methods' => static::get_method(),
'callback' => [ static::class, 'execute_static' ],
'permission_callback' => '__return_true',
],
);
}
);
}
abstract protected static function get_method(): string;
abstract protected function execute_endpoint( array $params ): array;
final public static function execute_static( WP_REST_Request $request ): WP_REST_Response {
$endpoint = new static();
return $endpoint->execute_with_validation( $request );
}
private function execute_with_validation( WP_REST_Request $request ): WP_REST_Response {
$fb_integration = facebook_for_woocommerce()->get_integration();
$offer_management_enabled_by_merchant = $fb_integration->is_facebook_managed_coupons_enabled();
if ( ! $offer_management_enabled_by_merchant ) {
$this->add_error( self::get_error_response_data( self::ERROR_OFFER_MANAGEMENT_DISABLED, 'Not enabled by seller' ) );
return $this->get_request_response( [], self::HTTP_FORBIDDEN );
}
$offer_management_enabled_by_fb = facebook_for_woocommerce()->get_rollout_switches()->is_switch_enabled(
RolloutSwitches::SWITCH_OFFER_MANAGEMENT_ENABLED
);
if ( ! $offer_management_enabled_by_fb ) {
$this->add_error( self::get_error_response_data( self::ERROR_OFFER_MANAGEMENT_DISABLED, 'Not enabled by Meta' ) );
return $this->get_request_response( [], self::HTTP_FORBIDDEN );
}
$jwt = $request->get_params()['jwt_params'] ?? null;
if ( null === $jwt ) {
$this->add_error( self::get_error_response_data( self::ERROR_JWT_NOT_FOUND ) );
return $this->get_request_response( [], self::HTTP_BAD_REQUEST );
}
$params = $this->get_decoded_request_params( $jwt );
if ( null === $params ) {
return $this->get_request_response( [], self::HTTP_UNAUTHORIZED );
}
$jwt_catalog_id = $params['aud'] ?? '';
if ( empty( $jwt_catalog_id ) || $jwt_catalog_id !== $fb_integration->get_product_catalog_id() ) {
$this->add_error(
self::get_error_response_data(
self::ERROR_CATALOG_ID_MISMATCH,
sprintf( 'Platform Catalog ID: %s, Request Catalog ID: %s', $fb_integration->get_product_catalog_id(), $jwt_catalog_id )
)
);
return $this->get_request_response( [], self::HTTP_FORBIDDEN );
}
try {
$response_data = $this->execute_endpoint( $params['payload'] );
return $this->get_request_response( $response_data );
} catch ( \Exception $ex ) {
$this->add_error( self::get_error_response_data( self::ERROR_OFFER_MANAGEMENT_ERROR, $ex->getMessage() ) );
return $this->get_request_response( [], self::HTTP_ERROR );
}
}
protected function get_decoded_request_params( string $jwt ): ?array {
try {
$decoded_params = RequestVerification::decode_jwt_with_retries( $jwt );
} catch ( JWTExpiredException $ex ) {
$this->add_error( self::get_error_response_data( self::ERROR_JWT_EXPIRED ) );
return null;
} catch ( \Exception $ex ) {
$this->add_error( self::get_error_response_data( self::ERROR_JWT_DECODE_FAILURE, $ex->getMessage() ) );
return null;
}
return $decoded_params;
}
protected static function get_offer_response_data( WC_Coupon $coupon ): array {
$is_percent_off = 'percent' === $coupon->get_discount_type();
$percent_off = $is_percent_off ? (int) $coupon->get_amount() : null;
$fixed_amount_off = $is_percent_off ? null : [
'amount' => $coupon->get_amount(),
'currency' => get_woocommerce_currency(),
];
$offer_class = 'order';
return array(
'offer_id' => $coupon->get_id(),
'code' => $coupon->get_code(),
'percent_off' => $percent_off,
'fixed_amount_off' => $fixed_amount_off,
'offer_class' => $offer_class,
'end_time' => $coupon->get_date_expires() ? $coupon->get_date_expires()->getTimestamp() : 0,
'usage_limit' => $coupon->get_usage_limit() ?? 0,
'usage_count' => $coupon->get_usage_count() ?? 0,
);
}
protected static function get_error_response_data( string $error_type, string $error_message = '', ?string $offer_code = null ): array {
return [
'error_type' => $error_type,
'error_message' => $error_message,
'offer_code' => $offer_code,
];
}
protected static function get_params_value_enforced( string $field_name, array $params ) {
if ( array_key_exists( $field_name, $params ) ) {
return $params[ $field_name ];
}
throw new \OutOfBoundsException( sprintf( 'Field: %s does not exist in request params. Params fields: %s', $field_name, wp_json_encode( array_keys( $params ) ) ) );
}
private function get_request_response( array $response_data, int $status_code = self::HTTP_OK ): WP_REST_Response {
$response_data = [
'data' => $response_data,
'errors' => $this->errors,
];
return new WP_REST_Response( $response_data, $status_code );
}
}