arch-8.x-1.x-dev/modules/cart/src/Form/CartForm.php

modules/cart/src/Form/CartForm.php
<?php

namespace Drupal\arch_cart\Form;

use Drupal\arch_cart\Cart\CartHandlerInterface;
use Drupal\arch_price\Price\PriceFactoryInterface;
use Drupal\arch_price\Price\PriceFormatterInterface;
use Drupal\arch_price\Price\PriceInterface;
use Drupal\arch_product\Entity\ProductInterface;
use Drupal\Component\Utility\Xss;
use Drupal\Core\Ajax\AjaxResponse;
use Drupal\Core\Ajax\InvokeCommand;
use Drupal\Core\Ajax\RemoveCommand;
use Drupal\Core\Ajax\ReplaceCommand;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Form\FormBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Language\LanguageManagerInterface;
use Drupal\Core\Render\MainContent\AjaxRenderer;
use Drupal\Core\Render\RendererInterface;
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\Core\Template\Attribute;
use Drupal\Core\Theme\ThemeManagerInterface;
use Drupal\Core\Url;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpFoundation\RequestStack;

/**
 * Cart form.
 *
 * @package Drupal\arch_cart\Form
 */
class CartForm extends FormBase {

  /**
   * The entity type manager service.
   *
   * @var \Drupal\Core\Entity\EntityTypeManagerInterface
   */
  protected $entityTypeManager;

  /**
   * Module handler.
   *
   * @var \Drupal\Core\Extension\ModuleHandlerInterface
   */
  protected $moduleHandler;

  /**
   * Theme manager.
   *
   * @var \Drupal\Core\Theme\ThemeManagerInterface
   */
  protected $themeManager;

  /**
   * The ajax renderer service.
   *
   * @var \Drupal\Core\Render\MainContent\AjaxRenderer
   */
  protected $ajaxRenderer;

  /**
   * Renderer.
   *
   * @var \Drupal\Core\Render\RendererInterface
   */
  protected $renderer;

  /**
   * Cart handler.
   *
   * @var \Drupal\arch_cart\Cart\CartHandlerInterface
   */
  protected $cartHandler;

  /**
   * Price factory.
   *
   * @var \Drupal\arch_price\Price\PriceFactoryInterface
   */
  protected $priceFactory;

  /**
   * Price formatter.
   *
   * @var \Drupal\arch_price\Price\PriceFormatterInterface
   */
  protected $priceFormatter;

  /**
   * Cart.
   *
   * @var \Drupal\arch_cart\Cart\CartInterface
   */
  protected $cart;

  /**
   * Language manager.
   *
   * @var \Drupal\Core\Language\LanguageManagerInterface
   */
  protected $languageManager;

  /**
   * Current language code.
   *
   * @var string
   */
  protected $currentLanguageCode;

  /**
   * Constructs a CartForm object.
   *
   * @param \Drupal\arch_cart\Cart\CartHandlerInterface $cart_handler
   *   Cart handler.
   * @param \Drupal\arch_price\Price\PriceFactoryInterface $price_factory
   *   Price factory.
   * @param \Drupal\arch_price\Price\PriceFormatterInterface $price_formatter
   *   Price formatter.
   * @param \Drupal\Core\Language\LanguageManagerInterface $language_manager
   *   Language manager.
   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
   *   The entity manager service.
   * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
   *   Module handler.
   * @param \Drupal\Core\Theme\ThemeManagerInterface $theme_manager
   *   Theme manager.
   * @param \Symfony\Component\HttpFoundation\RequestStack $request_stack
   *   The request stack service.
   * @param \Drupal\Core\Routing\RouteMatchInterface $route_match
   *   The current route match service.
   * @param \Drupal\Core\Render\RendererInterface $renderer
   *   Renderer.
   * @param \Drupal\Core\Render\MainContent\AjaxRenderer $ajax_renderer
   *   The ajax renderer service.
   */
  public function __construct(
    CartHandlerInterface $cart_handler,
    PriceFactoryInterface $price_factory,
    PriceFormatterInterface $price_formatter,
    LanguageManagerInterface $language_manager,
    EntityTypeManagerInterface $entity_type_manager,
    ModuleHandlerInterface $module_handler,
    ThemeManagerInterface $theme_manager,
    RequestStack $request_stack,
    RouteMatchInterface $route_match,
    RendererInterface $renderer,
    AjaxRenderer $ajax_renderer,
  ) {
    $this->entityTypeManager = $entity_type_manager;
    $this->moduleHandler = $module_handler;
    $this->themeManager = $theme_manager;
    $this->requestStack = $request_stack;
    $this->routeMatch = $route_match;
    $this->ajaxRenderer = $ajax_renderer;
    $this->renderer = $renderer;
    $this->cartHandler = $cart_handler;
    $this->priceFactory = $price_factory;
    $this->priceFormatter = $price_formatter;
    $this->languageManager = $language_manager;
    $this->currentLanguageCode = $language_manager->getCurrentLanguage()->getId();
    $this->cart = $cart_handler->getCart();
  }

  /**
   * {@inheritdoc}
   */
  public static function create(ContainerInterface $container) {
    return new static(
      $container->get('arch_cart_handler'),
      $container->get('price_factory'),
      $container->get('price_formatter'),
      $container->get('language_manager'),
      $container->get('entity_type.manager'),
      $container->get('module_handler'),
      $container->get('theme.manager'),
      $container->get('request_stack'),
      $container->get('current_route_match'),
      $container->get('renderer'),
      $container->get('main_content_renderer.ajax')
    );
  }

  /**
   * {@inheritdoc}
   */
  public function getFormId() {
    return 'arch_cart_form';
  }

  /**
   * Get product entity storage.
   *
   * @return \Drupal\Core\Entity\EntityStorageInterface
   *   Product entity storage.
   *
   * @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
   * @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
   */
  protected function getProductStorage() {
    return $this->entityTypeManager->getStorage('product');
  }

  /**
   * Get file entity storage.
   *
   * @return \Drupal\Core\Entity\EntityStorageInterface
   *   File entity storage.
   *
   * @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
   * @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
   */
  protected function getFileStorage() {
    return $this->entityTypeManager->getStorage('file');
  }

  /**
   * Get cart.
   *
   * @return \Drupal\arch_cart\Cart\CartInterface
   *   Cart instance.
   */
  protected function getCart() {
    if (
      !$this->cart
      || !$this->cart->getModuleHandler()
    ) {
      $this->cart = $this->cartHandler->getCart();
    }
    return $this->cart;
  }

  /**
   * {@inheritdoc}
   */
  public function buildForm(array $form, FormStateInterface $form_state) {
    if (empty($this->getCart()->getProducts())) {
      $form['products'] = [
        '#type' => 'container',
        '#attributes' => [
          'class' => [
            'text-center',
            'cart-empty',
          ],
        ],
        'message' => [
          '#type' => 'html_tag',
          '#tag' => 'div',
          '#value' => $this->t('Your cart is currently empty.', [], ['context' => 'arch_cart']),
        ],
      ];
      return $form;
    }

    $form = [];
    $form['#cart'] = $this->getCart();
    $form['#prefix'] = '<div id="arch-cart-form-wrapper" class="arch-cart-form-wrapper">';
    $form['#suffix'] = '</div>';

    $form['#attributes']['class'][] = 'arch-cart-form-form';

    $form['products'] = $this->buildProductsTable();

    $form['sum'] = [
      '#type' => 'container',
      '#attributes' => [
        'class' => [
          'cart-form-actions',
          'text-right',
          'clearfix',
        ],
      ],
    ];
    $form['sum']['totals'] = $this->buildCartTotals();

    $form['sum']['cart-actions'] = [
      '#type' => 'container',
      '#attributes' => [
        'class' => [
          'form-actions',
          'cart-actions-wrapper',
          'cart-form-actions__item',
          'clearfix',
        ],
      ],
      'back-button' => [
        '#type' => 'link',
        '#url' => NULL,
        '#title' => $this->t('Continue shopping', [], ['context' => 'arch_cart']),
        '#attributes' => [
          'class' => [
            'btn',
            'btn-default',
            'btn-continue-shopping',
          ],
        ],
      ],
      'checkout-button' => [
        '#type' => 'submit',
        '#value' => $this->t('Place order', [], ['context' => 'arch_cart']),
        '#disabled' => empty($this->getCart()->getCount()),
        '#attributes' => [
          'class' => [
            'btn-success',
            'btn-process-order',
          ],
        ],
      ],
    ];

    // Uses fromRoute instead of fromUri to being more flexible. E.g.: on a path
    // change on this product search view.
    $form['sum']['cart-actions']['back-button']['#url'] = Url::fromRoute('view.product_search.products');

    return $form;
  }

  /**
   * Price format settings.
   *
   * @return array
   *   Settings.
   */
  protected function getPriceFormatSettings() {
    return [
      'label' => FALSE,
      'vat_info' => FALSE,
    ];
  }

  /**
   * Build formatted price.
   *
   * @param \Drupal\arch_price\Price\PriceInterface $price
   *   Price instance.
   * @param string $mode
   *   Display mode.
   *
   * @return array
   *   Render array.
   */
  protected function renderPrice(PriceInterface $price, $mode) {
    return $this->priceFormatter->buildFormatted($price, $mode, $this->getPriceFormatSettings());
  }

  /**
   * Load product.
   *
   * @param int $pid
   *   Product ID.
   *
   * @return \Drupal\arch_product\Entity\ProductInterface
   *   Product with given ID.
   */
  protected function loadProduct($pid) {
    try {
      /** @var \Drupal\arch_product\Entity\ProductInterface $product */
      $product = $this->getProductStorage()->load($pid);
      if (!$product) {
        return NULL;
      }
      if ($product->hasTranslation($this->currentLanguageCode)) {
        $product = $product->getTranslation($this->currentLanguageCode);
      }
      return $product;
    }
    catch (\Exception $e) {
      return NULL;
    }
  }

  /**
   * Build products table.
   *
   * @return array
   *   Render array.
   */
  protected function buildProductsTable() {
    $build = [
      '#type' => 'table',
      '#header' => [],
    ];
    $build['#header']['product'] = [
      'data' => $this->t('Product', [], ['context' => 'arch_product']),
      'class' => 'th-product',
    ];
    $build['#header']['details'] = [
      'data' => '',
      'class' => 'th-details',
    ];
    $build['#header']['quantity'] = [
      'data' => $this->t('Quantity', [], ['context' => 'arch_cart']),
      'class' => 'th-quantity',
    ];
    $build['#header']['net_item_price'] = [
      'data' => $this->t('Net price', [], ['context' => 'arch_price']),
      'class' => 'th-net-price th-net-item-price th-item-price',
    ];
    $build['#header']['gross_item_price'] = [
      'data' => $this->t('Gross price', [], ['context' => 'arch_price']),
      'class' => 'th-gross-price th-gross-item-price th-item-price',
    ];
    $build['#header']['net_total_price'] = [
      'data' => $this->t('Total net price', [], ['context' => 'arch_price']),
      'class' => 'th-net-price th-net-total-price th-total-price',
    ];
    $build['#header']['gross_total_price'] = [
      'data' => $this->t('Total gross price', [], ['context' => 'arch_price']),
      'class' => 'th-gross-price th-gross-total-price th-total-price',
    ];
    $build['#header']['remove'] = [
      'data' => '',
      'class' => 'th-remove',
    ];

    foreach ($this->getCart()->getProducts() as $key => $item) {
      $item_id = $item['type'] . ':' . $item['id'];
      /** @var \Drupal\arch_product\Entity\ProductInterface $product */
      $product = $this->loadProduct($item['id']);
      if (empty($product)) {
        continue;
      }

      $build[$item_id] = $this->buildProductsTableRow($key, $item, $product);
    }

    return $build;
  }

  /**
   * Build products table row.
   *
   * @param int $key
   *   Cart key.
   * @param array $item
   *   Line item.
   * @param \Drupal\arch_product\Entity\ProductInterface $product
   *   Product entity.
   *
   * @return array
   *   Render array.
   */
  protected function buildProductsTableRow($key, array $item, ProductInterface $product) {
    $item_id = $item['type'] . ':' . $item['id'];

    /** @var \Drupal\arch_price\Price\PriceInterface $price */
    $price = $product->getActivePrice();

    $build = [
      '#attributes' => [
        'class' => ['cart-product-row'],
        'data-cart-line-item-item-id' => $item_id,
        'data-cart-line-item-key' => $key,
        'data-cart-line-item-type' => $item['type'],
        'data-cart-line-item-id' => $item['id'],
      ],
      '#product' => $product,
    ];
    try {
      $product_label = $product->toLink()->toRenderable();
      $product_label['#attributes']['class'][] = 'product-name';
      $product_label['#attributes']['class'][] = 'product-link';
    }
    catch (\Exception $e) {
      $product_label = [
        '#type' => 'inline_template',
        '#template' => '<span{{ attributes }}>{{ label }}</span>',
        '#context' => [
          'label' => $product->label(),
          'attributes' => new Attribute([
            'class' => 'product-name',
          ]),
        ],
      ];
    }

    $details = $this->buildProductsTableRowProperties($item, $product);
    $build['key'] = [
      '#wrapper_attributes' => [
        'class' => ['td-product'],
      ],
      'item_type' => [
        '#type' => 'type',
        '#value' => $item['type'],
      ],
      'item_id' => [
        '#type' => 'type',
        '#value' => $item['id'],
      ],
      'pid' => [
        '#type' => 'value',
        '#value' => $product->id(),
      ],
      'key' => [
        '#type' => 'value',
        '#value' => $key,
      ],
      'label' => $product_label,
      'image' => $details['image'] ?? NULL,
    ];
    $build['properties'] = [
      '#wrapper_attributes' => [
        'class' => ['td-details'],
      ],
      'properties' => $details['properties'] ?? NULL,
    ];
    $build['quantity'] = [
      '#wrapper_attributes' => [
        'class' => ['td-quantity'],
      ],
      '#type' => 'number',
      '#default_value' => $item['quantity'],
      '#min' => 1,
      '#ajax' => [
        'event' => 'change',
        'callback' => [$this, 'changeArticleAmount'],
        'wrapper' => 'arch-cart-form-wrapper',
      ],
      '#attributes' => [
        'data-item-type' => $item['type'],
        'data-item-id' => $item['id'],
        'data-item-item-id' => $item_id,
        'data-item-key' => $key,
      ],
    ];

    $build += $this->buildProductsTableRowPrices($item['quantity'], $price);

    $build['remove'] = [
      '#wrapper_attributes' => [
        'class' => ['td-remove'],
      ],
      '#type' => 'button',
      '#value' => $this->t('Remove item', [], ['context' => 'arch_cart']),
      '#return_value' => [
        'item_id' => $item_id,
        'key' => $key,
        'type' => $item['type'],
        'id' => $item['id'],
      ],
      '#attributes' => [
        'data-item-type' => $item['type'],
        'data-item-id' => $item['id'],
        'data-item-key' => $key,
      ],
      '#name' => 'remove:' . $item_id,
      '#ajax' => [
        'callback' => [$this, 'removeArticle'],
        'wrapper' => 'arch-cart-form-wrapper',
      ],
    ];

    $alter_context = [
      'line_item' => $item,
      'product' => $product,
      'cart' => $this->cart,
    ];
    $this->moduleHandler->alter('arch_cart_product_table_row', $build, $alter_context);
    $this->themeManager->alter('arch_cart_product_table_row', $build, $alter_context);

    return $build;
  }

  /**
   * Build product properties.
   *
   * @param array $line_item
   *   Line item.
   * @param \Drupal\arch_product\Entity\ProductInterface $product
   *   Product instance.
   *
   * @return array
   *   Render array.
   */
  protected function buildProductsTableRowProperties(array $line_item, ProductInterface $product) {
    $alter_context = [
      'line_item' => $line_item,
      'product' => $product,
      'cart' => $this->cart,
    ];

    $details = [];

    $image = [];
    $this->moduleHandler->alter('arch_cart_product_image', $image, $alter_context);
    $this->themeManager->alter('arch_cart_product_image', $image, $alter_context);
    if (!empty($image)) {
      $details['image'] = $image;
    }

    $properties = [];
    $this->moduleHandler->alter('arch_cart_product_properties', $properties, $alter_context);
    $this->themeManager->alter('arch_cart_product_properties', $properties, $alter_context);
    if (!empty($properties)) {
      $details['properties'] = $properties;
    }

    $this->moduleHandler->alter('arch_cart_product_details', $details, $alter_context);
    $this->themeManager->alter('arch_cart_product_details', $details, $alter_context);
    return $details;
  }

  /**
   * Total cells for products table row.
   *
   * @param float $quantity
   *   Item quantity.
   * @param \Drupal\arch_price\Price\PriceInterface $price
   *   Item price.
   *
   * @return array
   *   Render array.
   */
  protected function buildProductsTableRowPrices($quantity, PriceInterface $price) {
    $total_price_values = $price->getValues();
    $total_price_values['net'] = $total_price_values['net'] * $quantity;
    $total_price_values['gross'] = $total_price_values['gross'] * $quantity;
    $total_price = $this->priceFactory->getInstance($total_price_values);

    $build['net_item_price'] = [
      '#wrapper_attributes' => [
        'class' => [
          'td-net-price',
          'td-net-item-price',
        ],
        'data-price-type' => 'net_item_price',
      ],
      'value' => $this->renderPrice($price, PriceInterface::FORMAT_NET),
    ];
    $build['gross_item_price'] = [
      '#wrapper_attributes' => [
        'class' => [
          'td-gross-price',
          'td-gross-item-price',
        ],
        'data-price-type' => 'gross_item_price',
      ],
      'value' => $this->renderPrice($price, PriceInterface::FORMAT_GROSS),
    ];

    $build['net_total_price'] = [
      '#wrapper_attributes' => [
        'class' => [
          'td-net-price',
          'td-net-total-price',
        ],
        'data-price-type' => 'net_total_price',
      ],
      'value' => $this->renderPrice($total_price, PriceInterface::FORMAT_NET),
    ];
    $build['gross_total_price'] = [
      '#wrapper_attributes' => [
        'class' => [
          'td-gross-price',
          'td-gross-total-price',
        ],
        'data-price-type' => 'gross_total_price',
      ],
      'value' => $this->renderPrice($total_price, PriceInterface::FORMAT_GROSS),
    ];
    return $build;
  }

  /**
   * Change article amount callback.
   *
   * @param array $form
   *   Form structure.
   * @param \Drupal\Core\Form\FormStateInterface $form_state
   *   Form state.
   *
   * @return array|\Drupal\Core\Ajax\AjaxResponse
   *   Response or render array.
   *
   * @throws \Twig_Error_Loader
   * @throws \Twig_Error_Runtime
   * @throws \Twig_Error_Syntax
   */
  public function changeArticleAmount(array &$form, FormStateInterface $form_state) {
    $trigger_element = $form_state->getTriggeringElement();
    $line_item_values = $trigger_element['#attributes'];
    $amount = (float) Xss::filter($trigger_element['#value']);

    if (empty($amount) || $amount <= 0) {
      return $this->doRemoveArticle($form, [
        'type' => $line_item_values['data-item-type'],
        'id' => $line_item_values['data-item-id'],
        'item_id' => $line_item_values['data-item-item-id'],
      ]);
    }

    return $this->doChangeArticleAmount($form, [
      'type' => $line_item_values['data-item-type'],
      'id' => $line_item_values['data-item-id'],
    ], $amount);
  }

  /**
   * Totals block.
   *
   * @return array
   *   Render array.
   */
  protected function buildCartTotals() {
    $price_format_settings = $this->getPriceFormatSettings();
    $total_cart_price = $this->getCart()->getTotalPrice();
    $grand_total_price = $this->getCart()->getGrandTotalPrice();
    return [
      '#theme' => 'cart_page_totals',
      '#cart' => $this->getCart(),
      '#total_price' => $total_cart_price,
      '#grand_total_price' => $grand_total_price,
      '#price_format_settings' => $price_format_settings,
    ];
  }

  /**
   * Remove article ajax callback.
   *
   * @param array $form
   *   Form.
   * @param \Drupal\Core\Form\FormStateInterface $form_state
   *   Form state.
   *
   * @return \Drupal\Core\Ajax\AjaxResponse
   *   Ajax response.
   */
  public function removeArticle(array &$form, FormStateInterface $form_state) {
    $trigger_element = $form_state->getTriggeringElement();
    $remove_item_values = $trigger_element['#return_value'];

    return $this->doRemoveArticle($form, $remove_item_values);
  }

  /**
   * Do change article amount.
   */
  protected function doChangeArticleAmount(array &$form, $line_item_values, $amount) {
    // We have to reread cart from storage before remove items.
    $this->cart = $this->cartHandler->getCart(TRUE);
    $this->getCart()->updateItemQuantityById($line_item_values['type'], $line_item_values['id'], $amount);

    $totals = $this->buildCartTotals();
    $form['sum']['totals'] = $totals;

    if ($this->ajaxRenderer) {
      // DO NOT re-render the whole form!
      /** @var \Drupal\Core\Ajax\AjaxResponse $response */
      $response = new AjaxResponse();

      $response->addCommand(
        new InvokeCommand('body', 'trigger', [
          'commerce.minicart.updated',
          [
            'count' => $this->getCart()->getCount(),
            'total' => $this->getCart()->getTotal()['net'],
            'grand_total' => $this->getCart()->getGrandTotal()['net'],
            'amount_updated' => TRUE,
          ],
        ])
      );
      $response->addCommand(
        new InvokeCommand('body', 'trigger', ['arch_cart_api_cart_update'])
      );

      $product = $this->loadProduct($line_item_values['id']);
      if ($product) {
        $price = $product->getActivePrice();
        $row_selector = '.cart-product-row[data-cart-line-item-item-id="' . $line_item_values['type'] . ':' . $line_item_values['id'] . '"]';
        foreach ($this->buildProductsTableRowPrices($amount, $price) as $field => $value) {
          $response->addCommand(new ReplaceCommand($row_selector . ' td[data-price-type="' . $field . '"] .price', $value['value']));
        }
      }

      $totals_rendered = (string) $this->renderer->render($totals);
      $response->addCommand(new ReplaceCommand('[data-drupal-selector="edit-sum"] .totals', $totals_rendered));

      return $response;
    }

    $form['#cache']['contexts'][] = 'user';
    $form['#cache']['contexts'][] = 'session';
    $form['#cache']['max-age'] = 0;
    // In this case, sending event to Google Analytics will be skipped.
    return $form;
  }

  /**
   * Do remove article.
   */
  protected function doRemoveArticle(array &$form, array $remove_item_values) {
    // We have to reread cart from storage before remove items.
    $this->cart = $this->cartHandler->getCart(TRUE);
    $this->getCart()->removeItemById($remove_item_values['type'], $remove_item_values['id']);

    // We have to update totals.
    $totals = $this->buildCartTotals();
    $form['sum']['totals'] = $totals;

    if ($this->ajaxRenderer) {
      // We have to drop row from product table.
      $response = new AjaxResponse();
      $response->addCommand(new RemoveCommand('[data-drupal-selector="edit-products"] [data-cart-line-item-item-id="' . $remove_item_values['item_id'] . '"]'));

      $totals_rendered = (string) $this->renderer->render($totals);
      $response->addCommand(new ReplaceCommand('[data-drupal-selector="edit-sum"] .totals', $totals_rendered));

      // Force minicart to update.
      $response->addCommand(
        new InvokeCommand('body', 'trigger', ['arch_cart_api_cart_update'])
      );

      return $response;
    }

    $form['#cache']['contexts'][] = 'user';
    $form['#cache']['contexts'][] = 'session';
    $form['#cache']['max-age'] = 0;
    // In this case, sending event to Google Analytics will be skipped.
    return $form;
  }

  /**
   * {@inheritdoc}
   */
  public function validateForm(array &$form, FormStateInterface $form_state) {
    // @todo Implement this.
  }

  /**
   * {@inheritdoc}
   */
  public function submitForm(array &$form, FormStateInterface $form_state) {
    $form_state->setRedirect('arch_checkout.checkout');
  }

}

Главная | Обратная связь

drupal hosting | друпал хостинг | it patrol .inc