db->prepare( 'SELECT * FROM %i WHERE `id` = %d LIMIT 1', $this->table( 'order_intents' ), $id ); $row = $this->db->get_row( $sql, ARRAY_A ); return is_array( $row ) ? $this->hydrate( $row ) : null; } /** * {@inheritDoc} * * @param Uuid $uuid Parameter. */ public function find_by_uuid( Uuid $uuid ): ?OrderIntent { $sql = $this->db->prepare( 'SELECT * FROM %i WHERE `uuid` = %s LIMIT 1', $this->table( 'order_intents' ), $uuid->to_string() ); $row = $this->db->get_row( $sql, ARRAY_A ); return is_array( $row ) ? $this->hydrate( $row ) : null; } /** * {@inheritDoc} * * @param string $token_hash Parameter. */ public function find_by_token_hash( string $token_hash ): ?OrderIntent { $sql = $this->db->prepare( 'SELECT * FROM %i WHERE `token_hash` = %s LIMIT 1', $this->table( 'order_intents' ), $token_hash ); $row = $this->db->get_row( $sql, ARRAY_A ); return is_array( $row ) ? $this->hydrate( $row ) : null; } /** * {@inheritDoc} * * @param OrderIntent $intent Parameter. */ public function save( OrderIntent $intent ): void { $data = [ 'uuid' => $intent->uuid()->to_string(), 'token_hash' => $intent->token_hash(), 'form_id' => $intent->form_id(), 'status' => $intent->status()->value, 'customer_draft' => $this->encode_meta( $intent->customer_draft() ), 'cart_id' => $intent->cart_id(), 'coupon_code' => $intent->coupon_code(), 'totals' => $this->encode_meta( $intent->totals() ), 'current_step' => $intent->current_step(), 'completed_steps' => $this->encode_list( $intent->completed_steps() ), 'locale' => $intent->locale(), 'customer_timezone' => $intent->customer_timezone(), 'ip_hash' => $intent->ip_hash(), 'user_agent_hash' => $intent->user_agent_hash(), 'referrer' => $intent->referrer(), 'expires_at' => null === $intent->expires_at() ? null : $this->format_datetime( $intent->expires_at() ), 'converted_order_id' => $intent->converted_order_id(), 'last_activity_at' => $this->format_datetime( $intent->last_activity_at() ), 'created_at' => $this->format_datetime( $intent->created_at() ), 'updated_at' => $this->format_datetime( $intent->updated_at() ), ]; if ( null === $intent->id() ) { $this->db->insert( $this->table( 'order_intents' ), $data ); $intent->assign_id( $this->last_insert_id() ); return; } $this->db->update( $this->table( 'order_intents' ), $data, [ 'id' => $intent->id() ] ); } /** * {@inheritDoc} * * @param int $id Parameter. * @param DateTimeImmutable $now Parameter. */ public function claim_converting( int $id, DateTimeImmutable $now ): bool { $sql = $this->db->prepare( 'UPDATE %i SET `status` = %s, `updated_at` = %s, `last_activity_at` = %s WHERE `id` = %d AND `status` = %s', $this->table( 'order_intents' ), OrderIntentStatus::Converting->value, $this->format_datetime( $now ), $this->format_datetime( $now ), $id, OrderIntentStatus::Open->value ); $this->query_prepared( $sql ); return 1 === (int) $this->db->rows_affected; } /** * {@inheritDoc} * * @param DateTimeImmutable $before Parameter. * @param int $limit Parameter. */ public function find_stale_open( DateTimeImmutable $before, int $limit ): array { return $this->find_by_status_before( OrderIntentStatus::Open, $before, $limit ); } /** * {@inheritDoc} * * @param DateTimeImmutable $before Parameter. * @param int $limit Parameter. */ public function find_expired_abandoned( DateTimeImmutable $before, int $limit ): array { return $this->find_by_status_before( OrderIntentStatus::Abandoned, $before, $limit ); } /** * {@inheritDoc} * * @param DateTimeImmutable $before Parameter. * @param int $limit Parameter. */ public function find_stuck_converting( DateTimeImmutable $before, int $limit ): array { return $this->find_by_status_before( OrderIntentStatus::Converting, $before, $limit ); } /** * {@inheritDoc} * * @param string $email Parameter. */ public function find_by_draft_email( string $email ): array { $needle = strtolower( trim( $email ) ); $sql = $this->db->prepare( 'SELECT * FROM %i WHERE `customer_draft` IS NOT NULL AND LOWER(JSON_UNQUOTE(JSON_EXTRACT(`customer_draft`, \'$.email\'))) = %s', $this->table( 'order_intents' ), $needle ); $rows = $this->db->get_results( $sql, ARRAY_A ); if ( ! is_array( $rows ) ) { return []; } $out = []; foreach ( $rows as $row ) { if ( is_array( $row ) ) { $out[] = $this->hydrate( $row ); } } return $out; } /** * {@inheritDoc} * * @param int $id Parameter. */ public function delete_cascade( int $id ): void { $intent = $this->find_by_id( $id ); if ( null === $intent ) { return; } $cart_id = $intent->cart_id(); if ( null !== $cart_id ) { $delete_items = $this->db->prepare( 'DELETE FROM %i WHERE `cart_id` = %d', $this->table( 'cart_items' ), $cart_id ); $this->query_prepared( $delete_items ); $delete_cart = $this->db->prepare( 'DELETE FROM %i WHERE `id` = %d', $this->table( 'carts' ), $cart_id ); $this->query_prepared( $delete_cart ); } $delete_tx = $this->db->prepare( 'DELETE FROM %i WHERE `order_intent_id` = %d', $this->table( 'transaction_intents' ), $id ); $this->query_prepared( $delete_tx ); // Cascade only — not a core meta write of business data (docs/19 §7). $delete_meta = $this->db->prepare( 'DELETE FROM %i WHERE `entity_type` = %s AND `entity_id` = %d', $this->table( 'entity_meta' ), 'order_intent', $id ); $this->query_prepared( $delete_meta ); $delete_intent = $this->db->prepare( 'DELETE FROM %i WHERE `id` = %d', $this->table( 'order_intents' ), $id ); $this->query_prepared( $delete_intent ); } /** * {@inheritDoc} * * @param OrderIntentStatus $status Status. * @param int $limit Max rows. * @return list */ public function find_by_status( OrderIntentStatus $status, int $limit ): array { $sql = $this->db->prepare( 'SELECT * FROM %i WHERE `status` = %s ORDER BY `last_activity_at` DESC LIMIT %d', $this->table( 'order_intents' ), $status->value, $limit ); $rows = $this->db->get_results( $sql, ARRAY_A ); if ( ! is_array( $rows ) ) { return []; } $out = []; foreach ( $rows as $row ) { if ( is_array( $row ) ) { $out[] = $this->hydrate( $row ); } } return $out; } /** * Shared status + activity lookup. * * @param OrderIntentStatus $status Status. * @param DateTimeImmutable $before Cut-off. * @param int $limit Max rows. * @return list */ private function find_by_status_before( OrderIntentStatus $status, DateTimeImmutable $before, int $limit ): array { $sql = $this->db->prepare( 'SELECT * FROM %i WHERE `status` = %s AND `last_activity_at` < %s ORDER BY `last_activity_at` ASC LIMIT %d', $this->table( 'order_intents' ), $status->value, $this->format_datetime( $before ), $limit ); $rows = $this->db->get_results( $sql, ARRAY_A ); if ( ! is_array( $rows ) ) { return []; } $out = []; foreach ( $rows as $row ) { if ( is_array( $row ) ) { $out[] = $this->hydrate( $row ); } } return $out; } /** * Encode a string list as JSON. * * @param list|null $values List. */ private function encode_list( ?array $values ): ?string { if ( null === $values ) { return null; } $json = wp_json_encode( array_values( $values ) ); return false === $json ? null : $json; } /** * Decode a JSON list of strings. * * @param string|null $json JSON. * @return list|null */ private function decode_list( ?string $json ): ?array { if ( null === $json || '' === $json ) { return null; } $decoded = json_decode( $json, true ); if ( ! is_array( $decoded ) ) { return null; } $out = []; foreach ( $decoded as $value ) { if ( is_string( $value ) ) { $out[] = $value; } } return $out; } /** * Map a database row to an entity. * * @param array $row Database row. */ private function hydrate( array $row ): OrderIntent { $completed = $this->decode_list( isset( $row['completed_steps'] ) ? (string) $row['completed_steps'] : null ); return new OrderIntent( (int) $row['id'], new Uuid( (string) $row['uuid'] ), (string) $row['token_hash'], null !== $row['form_id'] ? (int) $row['form_id'] : null, OrderIntentStatus::from( (string) $row['status'] ), $this->decode_meta( isset( $row['customer_draft'] ) ? (string) $row['customer_draft'] : null ), null !== $row['cart_id'] ? (int) $row['cart_id'] : null, null !== $row['coupon_code'] ? (string) $row['coupon_code'] : null, $this->decode_meta( isset( $row['totals'] ) ? (string) $row['totals'] : null ), null !== $row['current_step'] ? (string) $row['current_step'] : null, $completed, null !== $row['locale'] ? (string) $row['locale'] : null, null !== $row['customer_timezone'] ? (string) $row['customer_timezone'] : null, null !== $row['ip_hash'] ? (string) $row['ip_hash'] : null, null !== $row['user_agent_hash'] ? (string) $row['user_agent_hash'] : null, null !== $row['referrer'] ? (string) $row['referrer'] : null, null !== $row['expires_at'] && '' !== $row['expires_at'] ? $this->parse_datetime( (string) $row['expires_at'] ) : null, null !== $row['converted_order_id'] ? (int) $row['converted_order_id'] : null, $this->parse_datetime( (string) $row['last_activity_at'] ), $this->parse_datetime( (string) $row['created_at'] ), $this->parse_datetime( (string) $row['updated_at'] ), ); } }