| Current File : /home/digitaw/www/wp-content/plugins/event-tickets/src/Tickets/Commerce/Gateways/Square/Order.php |
<?php
/**
* Order class for the Square gateway.
*
* @since 5.24.0
*
* @package TEC\Tickets\Commerce\Gateways\Square
*/
namespace TEC\Tickets\Commerce\Gateways\Square;
use WP_Post;
use TEC\Tickets\Commerce\Abstract_Order;
use TEC\Tickets\Commerce\Order as Commerce_Order;
use TEC\Tickets\Commerce\Gateways\Square\Syncs\Remote_Objects;
use TEC\Tickets\Commerce\Gateways\Square\Syncs\Objects\Item;
use RuntimeException;
use Tribe__Tickets__Main as ET;
use TEC\Tickets\Commerce\Values\Precision_Value;
use TEC\Tickets\Commerce\Gateways\Square\Status;
use TEC\Tickets\Commerce\Gateways\Square\Settings;
use TEC\Tickets\Commerce\Settings as Commerce_Settings;
use TEC\Tickets\Commerce\Meta as Commerce_Meta;
use TEC\Tickets\Commerce\Ticket as Ticket_Data;
use Tribe__Tickets__Ticket_Object as Ticket_Object;
use Tribe__Repository;
use WP_User_Query;
use WP_User;
use TEC\Tickets\Exceptions\NotEnoughStockException;
use stdClass;
use TEC\Common\StellarWP\DB\DB;
/**
* Class Order.
*
* @since 5.24.0
*
* @package TEC\Tickets\Commerce\Gateways\Square
*/
class Order extends Abstract_Order {
/**
* The hook to pull the order.
*
* @since 5.24.0
*
* @var string
*/
public const HOOK_PULL_ORDER_ACTION = 'tec_tickets_commerce_square_order_pull_order';
/**
* The merchant object.
*
* @since 5.24.0
*
* @var Merchant
*/
private Merchant $merchant;
/**
* The commerce order object.
*
* @since 5.24.0
*
* @var Commerce_Order
*/
private Commerce_Order $commerce_order;
/**
* The settings object.
*
* @since 5.24.0
*
* @var Settings
*/
private Settings $settings;
/**
* Order constructor.
*
* @since 5.24.0
*
* @param Merchant $merchant The merchant object.
* @param Commerce_Order $commerce_order The commerce order object.
* @param Settings $settings The settings object.
*/
public function __construct( Merchant $merchant, Commerce_Order $commerce_order, Settings $settings ) {
$this->merchant = $merchant;
$this->commerce_order = $commerce_order;
$this->settings = $settings;
}
/**
* Filter the schema for the repository.
*
* @since 5.24.0
*
* @param array $schema The schema.
* @param Tribe__Repository $repository The repository.
*
* @return array
*/
public function filter_schema( array $schema = [], ?Tribe__Repository $repository = null ) {
$schema['square_payment_id'] = function ( $payment_ids ) use ( $repository ) {
$this->filter_by_payment_id( $payment_ids, $repository );
};
$schema['square_payment_id_not'] = function ( $payment_ids ) use ( $repository ) {
$this->filter_by_payment_id_not( $payment_ids, $repository );
};
$schema['square_refund_id'] = function ( $refund_ids ) use ( $repository ) {
$this->filter_by_refund_id( $refund_ids, $repository );
};
$schema['square_refund_id_not'] = function ( $refund_ids ) use ( $repository ) {
$this->filter_by_refund_id_not( $refund_ids, $repository );
};
return $schema;
}
/**
* Create a Square order from a Commerce order.
*
* @since 5.24.0
*
* @param WP_Post $order The order object.
*
* @return string The Square order ID.
*
* @throws RuntimeException If the order fails to be created or updated.
*/
public function upsert_square_from_local_order( WP_Post $order ): string {
$square_order = [
'location_id' => $this->merchant->get_location_id(),
'reference_id' => (string) $order->ID,
'state' => 'OPEN',
];
$remote_objects = tribe( Remote_Objects::class );
/**
* Filters the customer ID for the Square order.
*
* @since 5.24.0
*
* @param string $customer_id The customer ID.
* @param WP_Post $order The order object.
*/
$customer_id = (string) apply_filters( 'tec_tickets_commerce_square_order_customer_id', $remote_objects->get_customer_id( $order->ID ), $order );
if ( $customer_id ) {
$square_order['customer_id'] = $customer_id;
}
/**
* Filters the metadata for the Square order.
*
* @since 5.24.0
*
* @param array $metadata The metadata for the Square order.
*/
$square_order['metadata'] = apply_filters(
'tec_tickets_commerce_square_order_metadata',
[
'created_source' => home_url(),
'et_version' => ET::VERSION,
],
$order
);
$square_order = $this->add_items_to_square_payload( $square_order, $order );
/**
* Filters the Square order payload.
*
* @since 5.24.0
*
* @param array $payload The payload for the Square order.
* @param WP_Post $order The order object.
*/
$square_order = apply_filters( 'tec_tickets_commerce_square_order_payload', $square_order, $order );
$square_order_id = null;
if ( $order->gateway_order_id ?? false ) {
$square_order_id = $order->gateway_order_id;
}
if ( $square_order_id && ! $this->needs_update( $square_order, $order->ID ) ) {
return $square_order_id;
}
if ( $square_order_id ) {
$order_version = (int) ( $order->gateway_order_version ?? 0 );
$square_order['version'] = $order_version ? $order_version : 1;
}
$body = [
'idempotency_key' => uniqid( 'tec-square-calculate-', true ),
'order' => $square_order,
];
$calculated_order = Requests::post(
'orders/calculate',
[],
[
'body' => $body,
]
);
if ( empty( $calculated_order['order']['total_money']['amount'] ) ) {
do_action( 'tribe_log', 'error', 'Square order calculate failed', [ $calculated_order['errors'] ?? $calculated_order, $square_order, $square_order_id ] );
throw new RuntimeException( __( 'Failed to calculate the Square order.', 'event-tickets' ), 1 );
}
$calculated_total = (int) $calculated_order['order']['total_money']['amount'];
$local_total = (int) ( 100 * (float) $order->total );
$diff = $calculated_total - $local_total;
if ( 0 !== $diff ) {
if ( $diff > 0 ) {
if ( ! ( isset( $body['order']['discounts'] ) && is_array( $body['order']['discounts'] ) ) ) {
$body['order']['discounts'] = [];
}
$body['order']['discounts'][] = [
'name' => __( 'Rounding difference discount', 'event-tickets' ),
'type' => 'FIXED_AMOUNT',
'scope' => 'ORDER',
'amount_money' => [
'amount' => absint( $diff ),
'currency' => $order->currency,
],
];
} else {
if ( ! ( isset( $body['order']['service_charges'] ) && is_array( $body['order']['service_charges'] ) ) ) {
$body['order']['service_charges'] = [];
}
$body['order']['service_charges'][] = [
'name' => __( 'Rounding difference service charge', 'event-tickets' ),
'calculation_phase' => 'SUBTOTAL_PHASE',
'amount_money' => [
'amount' => absint( $diff ),
'currency' => $order->currency,
],
];
}
}
$body['idempotency_key'] = uniqid( $square_order_id ? 'tec-square-update-' : 'tec-square-create-', true );
/**
* Fires before the Square order is upserted.
*
* @since 5.24.0
*
* @param array $square_order The Square order.
* @param int $order_id The order ID.
*/
do_action( 'tec_tickets_commerce_square_order_before_upsert', $square_order, $order->ID );
$response = Requests::request(
$square_order_id ? 'PUT' : 'POST',
sprintf( 'orders%s', $square_order_id ? "/{$square_order_id}" : '' ),
[],
[
'body' => $body,
]
);
if ( empty( $response['order']['id'] ) ) {
do_action( 'tribe_log', 'error', 'Square order upsert failed', [ $response['errors'] ?? $response, $square_order, $square_order_id ] );
throw new RuntimeException( __( 'Failed to create or update Square order.', 'event-tickets' ), 1 );
}
$args = [
'gateway_order_id' => $response['order']['id'],
'gateway_customer_id' => $customer_id,
'gateway_order_version' => $response['order']['version'],
'latest_payload_hash_sent' => md5( wp_json_encode( $square_order ) ),
'gateway_order_object' => wp_json_encode( $response['order'] ),
];
// Update the order with the new Square order ID.
$order_updated = tec_tc_orders()->by( 'id', $order->ID )->set_args(
$args
)->save();
if ( ! $order_updated || ! isset( $order_updated[ $order->ID ] ) || ! $order_updated[ $order->ID ] ) {
do_action( 'tribe_log', 'error', 'Order update failed', [ $args, $order ] );
throw new RuntimeException( __( 'Failed to update the order with the new Square order ID.', 'event-tickets' ), 2 );
}
/**
* Fires after the Square order is upserted.
*
* @since 5.24.0
*
* @param array $response The Square order.
* @param int $order_id The order ID.
* @param array $square_order The Square order.
*/
do_action( 'tec_tickets_commerce_square_order_after_upsert', $response['order'], $order->ID, $square_order );
tribe( Syncs\Regulator::class )->schedule( self::HOOK_PULL_ORDER_ACTION, [ $response['order']['id'] ], 2 * MINUTE_IN_SECONDS );
return $response['order']['id'];
}
/**
* Upsert a local order from a Square order.
*
* @since 5.24.0
*
* @param string $square_order_id The Square order ID.
* @param array $event_data The event data.
*
* @return ?WP_Post
*/
public function upsert_local_from_square_order( string $square_order_id, array $event_data = [] ): ?WP_Post {
$square_order = $this->get_square_order( $square_order_id );
if ( empty( $square_order['order'] ) ) {
return null;
}
$square_order = $square_order['order'];
$order = null;
$ref_id = $square_order['reference_id'] ?? false;
if ( $ref_id && is_numeric( $ref_id ) ) {
$order = tec_tc_get_order( $ref_id );
$is_update = $order instanceof WP_Post;
}
$callable = empty( $square_order['refunds'] ) ? [ tribe( Commerce_Order::class ), 'get_from_gateway_order_id' ] : [ $this, 'get_by_refund_id' ];
$args = empty( $square_order['refunds'] ) ? [ $square_order_id ] : [ $square_order['refunds']['0']['id'] ];
$order = $order instanceof WP_Post ? $order : call_user_func( $callable, $args );
$is_update = $order instanceof WP_Post;
if ( ! $is_update && empty( $square_order['refunds'] ) ) {
$order = $this->get_by_original_gateway_order_id( $square_order_id );
$is_update = $order instanceof WP_Post;
}
if ( ! $is_update && ! $this->settings->is_inventory_sync_enabled() ) {
// When the sync is not enabled, we listen ONLY for UPDATES to existing orders to our DB.
return null;
}
if ( $is_update && $order->gateway_order_id !== $square_order_id && empty( $square_order['refunds'] ) ) {
// The order has been changed in a way that now is being matched with a different Square order.
// For example, that's possible when an order has been refunded. The refund is a new Square order,
// which we store in `gateway_order_id` property.
return null;
}
if ( ! $is_update ) {
try {
$items = $this->get_items_from_square_order( $square_order_id );
} catch ( NotEnoughStockException $e ) {
do_action( 'tribe_log', 'warning', 'Not enough stock for incoming order - refunding the order.', [ $square_order_id ] );
$this->refund_remote_order( $square_order_id );
return null;
}
if ( empty( $items ) ) {
// We don't create orders without at least one item we recognize.
return null;
}
$missed_money = $items['missed_money'] ?? 0;
unset( $items['missed_money'] );
$net_amounts = $square_order['net_amounts'];
$total_value = new Precision_Value( $net_amounts['total_money']['amount'] / 100 );
$subtotal_value = $net_amounts['total_money']['amount'] - $net_amounts['tax_money']['amount'] - $net_amounts['tip_money']['amount'] - $net_amounts['service_charge_money']['amount'];
$subtotal_value += $net_amounts['discount_money']['amount'];
$subtotal_value = new Precision_Value( $subtotal_value / 100 );
$hash = md5( microtime() . wp_json_encode( $items ) );
$purchaser = $this->get_square_orders_customer( $square_order_id );
$order_args = [
'total_value' => $total_value->get(),
'subtotal' => $subtotal_value->get(),
'currency' => $net_amounts['total_money']['currency'],
'hash' => $hash,
'items' => $items,
'gateway' => Gateway::get_key(),
'title' => $this->commerce_order->generate_order_title( $items, $hash ),
'purchaser_user_id' => $purchaser->ID ?? 0,
'purchaser_full_name' => $purchaser->display_name ?? '',
'purchaser_first_name' => $purchaser->first_name ?? '',
'purchaser_last_name' => $purchaser->last_name ?? '',
'purchaser_email' => $purchaser->user_email ?? '',
'gateway_order_id' => $square_order_id,
'gateway_order_version' => $square_order['version'] ?? 1,
'gateway_order_object' => wp_json_encode( $square_order ),
];
$duplicate_order = $this->get_by_original_gateway_order_id( $square_order_id );
if ( $duplicate_order instanceof WP_Post ) {
return $duplicate_order;
}
DB::beginTransaction();
$order = $this->commerce_order->create( tribe( Gateway::class ), $order_args );
$order_ids = $this->commerce_order->get_order_ids_from_gateway_order_id( $square_order_id );
if ( count( $order_ids ) > 1 ) {
do_action( 'tribe_log', 'warning', 'Multiple orders found for the same Square order ID - Deleting: ' . $order->ID, [ $order_ids, $square_order_id ] );
DB::rollback();
return null;
}
DB::commit();
update_post_meta( $order->ID, Commerce_Order::META_ORDER_TOTAL_AMOUNT_UNACCOUNTED, $missed_money );
update_post_meta( $order->ID, Commerce_Order::META_ORDER_TOTAL_TAX, ( new Precision_Value( $net_amounts['tax_money']['amount'] / 100 ) )->get() );
update_post_meta( $order->ID, Commerce_Order::META_ORDER_TOTAL_TIP, ( new Precision_Value( $net_amounts['tip_money']['amount'] / 100 ) )->get() );
update_post_meta( $order->ID, Commerce_Order::META_ORDER_CREATED_BY, 'square-pos' );
}
if ( ! $order instanceof WP_Post ) {
return null;
}
$event_id = $event_data['event_id'] ?? '';
if ( $event_id ) {
Commerce_Meta::add( $order->ID, REST\Webhook_Endpoint::KEY_ORDER_WEBHOOK_IDS, $event_id, [], 'post', false );
}
$payments = $square_order['tenders'] ?? [];
if ( ! empty( $payments ) ) {
$order_payments = array_flip( $this->get_payment_ids( $order ) );
foreach ( $payments as $payment ) {
$payment_id = $payment['id'] ?? false;
if ( ! $payment_id ) {
continue;
}
if ( isset( $order_payments[ $payment_id ] ) ) {
continue;
}
$this->add_payment_id( $order, $payment_id );
}
}
$refunds = $square_order['refunds'] ?? [];
if ( ! empty( $refunds ) ) {
$order_refunds = array_flip( $this->get_refund_ids( $order ) );
foreach ( $refunds as $refund ) {
$refund_id = $refund['id'] ?? false;
if ( ! $refund_id ) {
continue;
}
if ( isset( $order_refunds[ $refund_id ] ) ) {
continue;
}
$this->add_refund_id( $order, $refund_id );
}
}
$status = $square_order['state'] ?? false;
if ( ! $status ) {
do_action(
'tribe_log',
'error',
'Square order webhook - no status found',
[
'source' => 'tickets-commerce-square',
'square_id' => $square_order_id,
'order_id' => $order->ID,
'event_data' => $event_data,
]
);
return $order;
}
$additional_data = [];
if ( ! empty( $square_order['refunds'] ) ) {
if ( 'COMPLETED' !== $status ) {
return $order;
}
$status = 'REFUNDED';
$payload = [];
foreach ( $square_order['refunds'] as $refund ) {
$payload[]['data']['object']['refund'] = $refund;
}
$additional_data = [
'gateway_payload' => $payload,
];
if ( $order->gateway_order_id !== $square_order_id ) {
$additional_data['gateway_order_id'] = $square_order_id;
$additional_data['original_gateway_order_id'] = $order->gateway_order_id;
}
}
$status_obj = tribe( Status::class )->convert_to_commerce_status( $status );
if ( ! $status_obj ) {
do_action(
'tribe_log',
'error',
'Square order webhook - no matching status found',
[
'source' => 'tickets-commerce-square',
'square_id' => $square_order_id,
'order_id' => $order->ID,
'status' => $status,
'event_data' => $event_data,
]
);
return $order;
}
if ( time() < $order->on_checkout_hold ) {
tribe( Webhooks::class )->add_pending_webhook( $order->ID, $status_obj->get_wp_slug(), $order->post_status, [ 'gateway_payload' => $event_data ] );
as_schedule_single_action(
$order->on_checkout_hold + MINUTE_IN_SECONDS,
'tec_tickets_commerce_async_webhook_process',
[
'order_id' => $order->ID,
'try' => 0,
],
'tec-tickets-commerce-webhooks'
);
return $order;
}
$this->commerce_order->modify_status( $order->ID, $status_obj->get_slug(), array_merge( $event_data ? [ 'gateway_payload' => $event_data ] : [], $additional_data ) );
return $order;
}
/**
* Get the cached remote data for an order.
*
* @since 5.24.0
*
* @param int $order_id The order ID.
* @param string $local_id The local ID.
* @param string $type The type of data to get.
*
* @return array
*/
public function get_cached_remote_data( int $order_id, string $local_id, string $type = 'line_items' ): array {
$cache = tribe_cache();
$cache_key = 'tec_tickets_commerce_square_order_' . $order_id;
$cached_data = $cache[ $cache_key ] ?? false;
if ( ! is_array( $cached_data ) ) {
$order = tec_tc_get_order( $order_id );
if ( ! $order instanceof WP_Post ) {
return [];
}
$cached_data = $order->gateway_order_object ?? [];
$cache[ $cache_key ] = $cached_data;
}
$items = $cached_data[ $type ] ?? [];
foreach ( $items as $item ) {
$stored_id = $item['metadata']['local_id'] ?? false;
if ( ! $stored_id ) {
continue;
}
if ( $stored_id !== $local_id ) {
continue;
}
return $item;
}
return [];
}
/**
* Get the URL to view the order in Square dashboard.
*
* @since 5.24.0
*
* @param WP_Post $order The order object.
*
* @return string
*/
public function get_gateway_dashboard_url_by_order( WP_Post $order ): ?string {
if ( ! $this->merchant->is_active() ) {
return '';
}
$order_id = $order->gateway_order_id ?? false;
if ( ! $order_id ) {
return '';
}
$is_test_mode = tribe( Gateway::class )->is_test_mode();
return sprintf( 'https://app.squareup%s.com/dashboard/orders/overview/%s', $is_test_mode ? 'sandbox' : '', $order_id );
}
/**
* Check if the order needs to be updated.
*
* @since 5.24.0
*
* @param array $square_order The Square order.
* @param int $order_id The order ID.
*
* @return bool
*/
public function needs_update( array $square_order, int $order_id ): bool {
/**
* Filters if the Square order needs to be updated.
*
* @since 5.24.0
*
* @param bool $needs_update Whether the Square order needs to be updated.
*/
return apply_filters(
'tec_tickets_commerce_square_order_needs_update',
md5( wp_json_encode( $square_order ) ) !== Commerce_Meta::get( $order_id, Commerce_Order::LATEST_PAYLOAD_HASH_SENT_TO_GATEWAY_META_KEY, [], 'post', true, false ),
$square_order,
$order_id
);
}
/**
* Add items to the Square payload.
*
* @since 5.24.0
*
* @param array $square_order The Square order.
* @param WP_Post $order The order object.
*
* @return array
*/
public function add_items_to_square_payload( array $square_order, WP_Post $order ): array {
$remote_objects = tribe( Remote_Objects::class );
$line_items = [];
foreach ( $order->fees as $fee ) {
if ( ! isset( $line_items['service_charges'] ) ) {
$line_items['service_charges'] = [];
}
$charge = $remote_objects->get_service_charge( $fee, $order );
if ( empty( $charge ) ) {
continue;
}
$line_items['service_charges'][] = $charge;
}
foreach ( $order->coupons as $coupon ) {
if ( ! isset( $line_items['discounts'] ) ) {
$line_items['discounts'] = [];
}
$discount = $remote_objects->get_discount( $coupon, $order );
if ( empty( $discount ) ) {
continue;
}
$line_items['discounts'][] = $discount;
}
foreach ( $order->items as $item ) {
if ( ! isset( $line_items['line_items'] ) ) {
$line_items['line_items'] = [];
}
$line_item = $remote_objects->get_line_item( $item, $order );
if ( empty( $line_item ) ) {
continue;
}
$line_items['line_items'][] = $line_item;
}
return array_merge( $square_order, $line_items );
}
/**
* Get the items from the Square order.
*
* @since 5.24.0
*
* @param string $square_order_id The Square order ID.
*
* @return array
*
* @throws NotEnoughStockException If the stock is not enough.
*/
public function get_items_from_square_order( string $square_order_id ): array {
$square_order = $this->get_square_order( $square_order_id );
if ( empty( $square_order['order'] ) ) {
return [];
}
$square_order = $square_order['order'];
$items = [];
// This corresponds to our tickets.
$tickets = $square_order['line_items'] ?? [];
$missed_money = 0;
foreach ( $tickets as $ticket ) {
$object_id = $ticket['metadata']['local_id'] ?? false;
if ( ! is_numeric( $object_id ) ) {
$object_id = Commerce_Meta::get_object_id( Item::SQUARE_ID_META, $ticket['catalog_object_id'] );
}
$ticket_obj = tribe( Ticket_Data::class )->load_ticket_object( $object_id );
if ( ! $ticket_obj instanceof Ticket_Object ) {
$second_id = Commerce_Meta::get_object_id( Item::SQUARE_ID_META, $ticket['catalog_object_id'] );
if ( $second_id === $object_id ) {
$missed_money += $ticket['variation_total_price_money']['amount'];
continue;
}
$object_id = $second_id;
}
$ticket_obj = tribe( Ticket_Data::class )->load_ticket_object( $object_id );
if ( ! $ticket_obj instanceof Ticket_Object ) {
$missed_money += $ticket['variation_total_price_money']['amount'];
continue;
}
$quantity = $ticket['quantity'] ?? 1;
/**
* Filters whether to prevent overselling or not.
*
* @since 5.24.0
*
* @param bool $prevent_overselling Whether to prevent overselling or not.
* @param Ticket_Object $ticket_obj The ticket object.
* @param int $quantity The quantity of the ticket.
*/
if ( -1 !== $ticket_obj->available() && $quantity > $ticket_obj->available() && apply_filters( 'tec_tickets_commerce_square_prevent_overselling', true, $ticket_obj, $quantity ) ) {
throw new NotEnoughStockException( sprintf( 'Not enough stock for ticket %s', $ticket_obj->ID ) );
}
$items[] = [
'event_id' => $ticket_obj->get_event_id(),
'price' => ( new Precision_Value( $ticket['base_price_money']['amount'] / 100 ) )->get(),
'quantity' => $quantity,
'ticket_id' => $ticket_obj->ID,
'regular_price' => $ticket_obj->regular_price,
'regular_sub_total' => ( new Precision_Value( $ticket_obj->regular_price * ( $ticket['quantity'] ?? 1 ) ) )->get(),
'sub_total' => ( new Precision_Value( $ticket['variation_total_price_money']['amount'] / 100 ) )->get(),
'type' => 'ticket',
];
}
if ( ! $items ) {
// If no ticket was found, we bail.
return [];
}
if ( $missed_money > 0 ) {
$items['missed_money'] = ( new Precision_Value( $missed_money / 100 ) )->get();
}
// This corresponds to our coupons.
$coupons = $square_order['discounts'] ?? [];
foreach ( $coupons as $offset => $coupon ) {
$items[] = [
'id' => $coupon['metadata']['local_id'] ?? 0,
'type' => 'coupon',
'coupon_id' => $coupon['metadata']['local_id'] ?? 0,
'price' => ( new Precision_Value( $coupon['applied_money']['amount'] / 100 ) )->get(),
'sub_total' => ( new Precision_Value( -1 * $coupon['applied_money']['amount'] / 100 ) )->get(),
// translators: %d is the offset of the coupon.
'display_name' => sprintf( __( 'Square Applied Discount %d', 'event-tickets' ), (int) $offset + 1 ),
'slug' => sprintf( 'square-applied-discount-%d', (int) $offset + 1 ),
'quantity' => 1,
'event_id' => 0,
'ticket_id' => 0,
];
}
// Our booking fees are supports as service charges.
$booking_fees = $square_order['service_charges'] ?? [];
foreach ( $booking_fees as $fee ) {
$items[] = [
'id' => $fee['metadata']['local_id'] ?? 0,
'type' => 'fee',
'price' => ( new Precision_Value( $fee['applied_money']['amount'] / 100 ) )->get(),
'sub_total' => ( new Precision_Value( $fee['applied_money']['amount'] / 100 ) )->get(),
'fee_id' => $fee['metadata']['local_id'] ?? 0,
'display_name' => $fee['name'],
'ticket_id' => 0,
'event_id' => 0,
'quantity' => 1,
];
}
$taxes = $square_order['taxes'] ?? [];
// We don's support taxes yet in TC, but ADDITIVE taxes need to be added as a separate item to the
// order so that the total is reflecting reality. We add them as prefixed booking fees for now.
foreach ( $taxes as $tax ) {
if ( $tax['type'] !== 'ADDITIVE' ) {
continue;
}
$items[] = [
'id' => 'square-tax-' . $tax['uid'],
'type' => 'fee',
'price' => ( new Precision_Value( $tax['applied_money']['amount'] / 100 ) )->get(),
'sub_total' => ( new Precision_Value( $tax['applied_money']['amount'] / 100 ) )->get(),
'fee_id' => 'square-tax-' . $tax['uid'],
'display_name' => $tax['name'],
'ticket_id' => 0,
'event_id' => 0,
'quantity' => 1,
];
}
return $items;
}
/**
* Get the customer from the Square order.
*
* @since 5.24.0
*
* @param string $square_order_id The Square order ID.
*
* @return WP_User|null
*/
public function get_square_orders_customer( string $square_order_id ): ?WP_User {
$square_order = $this->get_square_order( $square_order_id );
if ( empty( $square_order['order'] ) ) {
return null;
}
$customer_id = $square_order['order']['customer_id'] ?? false;
if ( ! $customer_id ) {
return null;
}
$user_query = new WP_User_Query(
[
'meta_key' => Commerce_Settings::get_key( '_tec_tickets_commerce_gateways_square_customer_id_%s' ),
'meta_value' => $customer_id, // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_value
'number' => 1,
]
);
if ( ! empty( $user_query->get_results() ) ) {
return $user_query->get_results()[0];
}
$remote_customer = $this->get_square_customer( $customer_id );
if ( empty( $remote_customer['customer'] ) ) {
return null;
}
$user_query = new WP_User_Query(
[
'search' => $remote_customer['customer']['email_address'],
'search_columns' => [ 'user_email' ],
]
);
if ( ! empty( $user_query->get_results() ) ) {
return $user_query->get_results()[0];
}
$user_obj = new stdClass();
$family_name = $remote_customer['customer']['family_name'] ?? '';
$user_obj->ID = 0;
$user_obj->user_email = $remote_customer['customer']['email_address'];
$user_obj->display_name = $remote_customer['customer']['given_name'] . ( $family_name ? ' ' . $family_name : '' );
$user_obj->first_name = $remote_customer['customer']['given_name'];
$user_obj->last_name = $family_name;
return new WP_User( $user_obj );
}
/**
* Get the Square order.
*
* @since 5.24.0
*
* @param string $square_order_id The Square order ID.
*
* @return array
*/
protected function get_square_order( string $square_order_id ): array {
$cache = tribe_cache();
$cache_key = 'tec_tickets_commerce_square_order_' . $square_order_id;
// Runtime cache it.
$square_order = $cache[ $cache_key ] ?? false;
$square_order = is_array( $square_order ) ?
$square_order :
Requests::request(
'GET',
sprintf( 'orders/%s', $square_order_id ),
[],
[]
);
if ( ! empty( $square_order['errors'] ) ) {
do_action( 'tribe_log', 'error', 'Square order not found', [ $square_order_id, $square_order['errors'] ] );
return [];
}
$cache[ $cache_key ] = $square_order;
return $square_order;
}
/**
* Add a payment to the order.
*
* @since 5.24.0
*
* @param WP_Post $order The order object.
* @param string $payment_id The payment ID.
*
* @return bool
*/
public function add_payment_id( WP_Post $order, string $payment_id ): bool {
$added = Commerce_Meta::add( $order->ID, Payment::KEY_ORDER_PAYMENT_ID, $payment_id, [], 'post', false );
if ( ! $added ) {
return false;
}
Commerce_Meta::set( $order->ID, Payment::KEY_ORDER_PAYMENT_ID_TIME, tec_get_current_milliseconds(), [ $payment_id ], 'post', false );
return (bool) $added;
}
/**
* Get the payment IDs.
*
* @since 5.24.0
*
* @param WP_Post $order The order object.
*
* @return string[]
*/
public function get_payment_ids( WP_Post $order ): array {
return (array) Commerce_Meta::get( $order->ID, Payment::KEY_ORDER_PAYMENT_ID, [], 'post', false, false );
}
/**
* Get the order by payment ID.
*
* @since 5.24.0
*
* @param string $payment_id The payment ID.
* @param array $status The status of the order.
*
* @return WP_Post|null
*/
public function get_by_payment_id( string $payment_id, array $status = [ 'any' ] ): ?WP_Post {
return tec_tc_orders()->by_args(
[
'square_payment_id' => $payment_id,
'status' => $status,
]
)->first();
}
/**
* Filters order by payment ID.
*
* @since
*
* @param string|string[] $payment_ids Which payment IDs we are filtering by.
* @param Tribe__Repository $repository The repository.
*
* @return null
*/
public function filter_by_payment_id( $payment_ids = null, ?Tribe__Repository $repository = null ) {
if ( empty( $payment_ids ) ) {
return null;
}
$payment_ids = array_filter( (array) $payment_ids );
if ( empty( $payment_ids ) ) {
return null;
}
$repository->by( 'meta_in', Payment::KEY_ORDER_PAYMENT_ID, $payment_ids );
return null;
}
/**
* Filters order by payment ID not.
*
* @since
*
* @param string|string[] $payment_ids Which payment IDs we are filtering by.
* @param Tribe__Repository $repository The repository.
*
* @return null
*/
public function filter_by_payment_id_not( $payment_ids = null, ?Tribe__Repository $repository = null ) {
if ( empty( $payment_ids ) ) {
return null;
}
$payment_ids = array_filter( (array) $payment_ids );
if ( empty( $payment_ids ) ) {
return null;
}
$repository->by( 'meta_not_in', Payment::KEY_ORDER_PAYMENT_ID, $payment_ids );
return null;
}
/**
* Add a refund ID to the order.
*
* @since 5.24.0
*
* @param WP_Post $order The order object.
* @param string $refund_id The refund ID.
*
* @return bool
*/
public function add_refund_id( WP_Post $order, string $refund_id ): bool {
$added = Commerce_Meta::add( $order->ID, Payment::KEY_ORDER_REFUND_ID, $refund_id, [], 'post', false );
if ( ! $added ) {
return false;
}
Commerce_Meta::set( $order->ID, Payment::KEY_ORDER_REFUND_ID_TIME, tec_get_current_milliseconds(), [ $refund_id ], 'post', false );
return (bool) $added;
}
/**
* Get the payment IDs.
*
* @since 5.24.0
*
* @param WP_Post $order The order object.
*
* @return string[]
*/
public function get_refund_ids( WP_Post $order ): array {
return (array) Commerce_Meta::get( $order->ID, Payment::KEY_ORDER_REFUND_ID, [], 'post', false, false );
}
/**
* Get the order by refund ID.
*
* @since 5.24.0
*
* @param string $refund_id The refund ID.
* @param array $status The status of the order.
*
* @return WP_Post|null
*/
public function get_by_refund_id( string $refund_id, array $status = [ 'any' ] ): ?WP_Post {
return tec_tc_orders()->by_args(
[
'square_refund_id' => $refund_id,
'status' => $status,
]
)->first();
}
/**
* Get the order by original gateway order ID.
*
* @since 5.24.0
*
* @param string $original_gateway_order_id The original gateway order ID.
* @param array $status The status of the order.
*
* @return WP_Post|null
*/
public function get_by_original_gateway_order_id( string $original_gateway_order_id, array $status = [ 'any' ] ): ?WP_Post {
return tec_tc_orders()->by_args(
[
'original_gateway_order_id' => $original_gateway_order_id,
'status' => $status,
]
)->first();
}
/**
* Filters order by refund ID.
*
* @since
*
* @param string|string[] $refund_ids Which refund IDs we are filtering by.
* @param Tribe__Repository $repository The repository.
*
* @return null
*/
public function filter_by_refund_id( $refund_ids = null, ?Tribe__Repository $repository = null ) {
if ( empty( $refund_ids ) ) {
return null;
}
$refund_ids = array_filter( (array) $refund_ids );
if ( empty( $refund_ids ) ) {
return null;
}
$repository->by( 'meta_in', Payment::KEY_ORDER_REFUND_ID, $refund_ids );
return null;
}
/**
* Filters order by refund ID not.
*
* @since
*
* @param string|string[] $refund_ids Which refund IDs we are filtering by.
* @param Tribe__Repository $repository The repository.
*
* @return null
*/
public function filter_by_refund_id_not( $refund_ids = null, ?Tribe__Repository $repository = null ) {
if ( empty( $refund_ids ) ) {
return null;
}
$refund_ids = array_filter( (array) $refund_ids );
if ( empty( $refund_ids ) ) {
return null;
}
$repository->by( 'meta_not_in', Payment::KEY_ORDER_REFUND_ID, $refund_ids );
return null;
}
/**
* Get the Square customer.
*
* @since 5.24.0
*
* @param string $customer_id The Square customer ID.
*
* @return array
*/
protected function get_square_customer( string $customer_id ): array {
$cache = tribe_cache();
$cache_key = 'tec_tickets_commerce_square_customer_' . $customer_id;
$remote_customer = $cache[ $cache_key ] ?? false;
if ( is_array( $remote_customer ) ) {
return $remote_customer;
}
$remote_customer = Requests::request(
'GET',
sprintf( 'customers/%s', $customer_id ),
[],
);
if ( ! empty( $remote_customer['errors'] ) ) {
do_action( 'tribe_log', 'error', 'Square customer not found', [ $customer_id, $remote_customer['errors'] ] );
return [];
}
$cache->set( $cache_key, $remote_customer, HOUR_IN_SECONDS );
return $remote_customer;
}
/**
* Refund an order.
*
* To refund a Square order, you have to grab the Square order. Then in the property `tenders` you have to refund every tender.
*
* @since 5.24.0
*
* @param WP_Post $order The order post object.
*
* @return void
*
* @throws RuntimeException If the order has no Square order ID or the Square order is not found.
*/
public function refund_order( WP_Post $order ): void {
if ( ! $order->gateway_order_id ) {
throw new RuntimeException( __( 'Order has no Square order ID.', 'event-tickets' ) );
}
$this->refund_remote_order( $order->gateway_order_id, $order );
}
/**
* Refund a remote order.
*
* @since 5.24.0
*
* @param string $square_order_id The Square order ID.
* @param WP_Post|null $order The order post object.
*
* @return void
*
* @throws RuntimeException If the Square order is not found.
*/
public function refund_remote_order( string $square_order_id, ?WP_Post $order = null ): void {
$square_order = $this->get_square_order( $square_order_id );
if ( empty( $square_order['order'] ) ) {
throw new RuntimeException( __( 'Square order not found.', 'event-tickets' ) );
}
$tenders = $square_order['order']['tenders'] ?? [];
foreach ( $tenders as $tender ) {
$id = $tender['id'] ?? null;
$amount = $tender['amount_money'] ?? null;
if ( ! ( $id && $amount ) ) {
continue;
}
$body = [
'idempotency_key' => md5( 'refund-' . $id ),
'payment_id' => $id,
'amount_money' => $amount,
];
$response = Requests::post(
'refunds',
[],
[ 'body' => $body ]
);
if ( empty( $response['refund'] ) ) {
do_action( 'tribe_log', 'error', 'Square refund failed', [ $body, $response ] );
continue;
}
if ( ! $order instanceof WP_Post ) {
continue;
}
$this->add_refund_id( $order, explode( '_', $response['refund']['id'] )[1] );
tribe( Syncs\Regulator::class )->schedule( self::HOOK_PULL_ORDER_ACTION, [ $response['refund']['order_id'] ], MINUTE_IN_SECONDS / 3 );
}
}
}