Drupal Commerce 2.x

Drupal Camp Cluj Napoca 2015

Presented by Tavi Toporjinschi / @vasike

Introductions

Tavi Toporjinschi

Drupal Developer for Commerce Guys UK
Drupal & Commerce Contributor

Drupal Commerce 2.x co-maintainers

Bojan Zivanovic (bojanz)
and Ryan Szrama (rszrama)

Our vision is for Drupal Commerce to be the number one
open source e-commerce platform in the world...

Powering truly flexible e-commerce.

Drupal Commerce 1.x

A brief history.

Built from scratch on Drupal 7

Minimalistic and flexible

Uses Views for all listings

Uses Rules for business logic

Relies on "essential contribs" and distributions to complete the experience

We're doing something right...

Drupal Commerce 2.x

Drupal Commerce 2.x Sprint

Validated the architecture with members of SensioLabs, Smile, Publicis Modem, OSInet, i-KOS, Adyax, Ekino, and others.

PHP Libraries

powered by CommerceGuys


We* started with research

*Led by Bojan Zivanovic.

  • Define the deficiencies in our own software
  • Existing */money libraries
  • Sonata / Sylius e-commerce bundles
  • Non-Symfony PHP applications
  • Non-PHP applications

Identifying our Weaknesses

  • Age old tax management issues
  • Incomplete price management API
  • Incomplete currency formatting rules
  • Address implementation stretched to the limit
  • Limited ability to collaborate outside of Drupal

Proposing Standalone Solutions

  • Internationalization (esp. currency formatting)
  • Address formatting / validation
  • Territory grouping (to support taxing)
  • Tax rate management (at least for VAT)
  • Price calculation and manipulation
Not just interfaces / classes but also unprecedented data.
Minimal dependencies.
Simple APIs with clear documentation and examples.
Full test coverage.
Reusable by any PHP based e-commerce application.

intl

  • Number Formatter, inspired by intl.
  • Currencies
  • Countries
  • Languages

Coming soon

  • Date Formatting

Drupal Commerce blog.

Currency handling

$ ¢ £ p ¥ ₤ ₧ € ₹ ₩ ₴ ₯ ₮ ₲ ₳ ₵ ₭ ₪ ₫

Currency formatting

12 345,99 €

12.345,99 €

€12,345.99

د.إ.‏ ٩٩٩٫٩٩

pricing

A price is a value object. Each operation (add, subtract, multiply, divide, round) produces a new price instance. All amounts are passed as strings, and manipulated using bcmath.

Creating prices


use CommerceGuys\Intl\Currency\CurrencyRepository;
use CommerceGuys\Pricing\Price;

$currencyRepository = new CurrencyRepository;
$currency = $currencyRepository->get('EUR');

$firstPrice  = new Price('99.99', $currency);
$secondPrice = new Price('100', $currency);
$thirdPrice  = new Price('20.307', $currency);
                        

Price operations


// Every operation produces a new Price instance.
$total = $firstPrice
->add($secondPrice)
->subtract($thirdPrice)
->multiply('4')
->divide('2');
echo $total; // 359.366  EUR
echo $total->round(); // 359.37  EUR
echo $total->round(Price::ROUND_HALF_UP, 1); // 359.4 EUR
echo $total->greaterThan($firstPrice); // true
                        

Currency Conversion


use CommerceGuys\Intl\Currency\CurrencyRepository;
use CommerceGuys\Pricing\Price;

$currencyRepository = new CurrencyRepository;
$eur = $currencyRepository->get('EUR');
$usd = $currencyRepository->get('USD');

// Use an external library to get an actual exchange rate.
$rate = 1;
$eurPrice = new Price('100', $eur);
$usdPrice = $eurPrice->convert($usd, $rate);
echo $usdPrice;
                        

Price Formatting


use CommerceGuys\Intl\Currency\CurrencyRepository;
use CommerceGuys\Intl\NumberFormat\NumberFormatRepository;
use CommerceGuys\Intl\Formatter\NumberFormatter;
use CommerceGuys\Pricing\Price;

$currencyRepository = new CurrencyRepository;
$currency = $currencyRepository->get('USD');
$price = new Price('99.99', $currency);

$numberFormatRepository = new NumberFormatRepository;
$numberFormat = $numberFormatRepository->get('en-US');

$currencyFormatter = new NumberFormatter($numberFormat, NumberFormatter::CURRENCY);
echo $currencyFormatter->formatCurrency($price->getAmount(), $price->getCurrency());
                        

addressing

  • Address formats for 200 countries
  • Subdivisions (administrative areas, localities, dependent localities) for 40 countries
  • Subdivision translations for all of the parent country's (i.e Canada, Switzerland) official languages.
  • Validation (via Symfony Validator)
  • Form generation (via Symfony Form)
  • Postal formatting

Featured on the Drupal Commerce blog.

Address Data


use CommerceGuys\Addressing\Repository\AddressFormatRepository;
use CommerceGuys\Addressing\Repository\SubdivisionRepository;

$addressFormatRepository = new AddressFormatRepository();
$subdivisionRepository = new SubdivisionRepository();

// Get the address format for Canada.
$addressFormat = $addressFormatRepository->get('CA');

// Get the subdivisions for Canada, in French.
$states = $subdivisionRepository->getAll('CA', 0, 'fr');
foreach ($states as $state) {
    echo $state->getName();
}
                        

What fields?

Somewhere else

Address format

  • Which fields are used, and in which order
  • Which fields are required
  • Which fields need to be uppercased
  • Field labels
  • Regular expression for validating postal codes

Address Formatting


use CommerceGuys\Addressing\Formatter\PostalFormatter;
use CommerceGuys\Addressing\Provider\DataProvider;

$dataProvider = new DataProvider();
$formatter = new PostalFormatter($dataProvider);

$address = new Address();
$address
    ->setCountryCode('CH')
    ->setPostalCode('8184')
    ->setAddressLine1('Schulweg 4')
    ->setLocality('Bachenbülach');

// Format an address for sending from Switzerland, in French.
// If the address destination is not Switzerland, the country name will be
// appended in French, uppercase.
echo $formatter->format($address, 'CH', 'fr');
                            

zone

Zones are territorial groupings mostly used for shipping or tax purposes.

A zone can match other zones, countries, subdivisions (states/provinces/municipalities), postal codes.

Create the German VAT zone


use CommerceGuys\Addressing\Model\Address;
use CommerceGuys\Zone\Model\Zone;
use CommerceGuys\Zone\Model\ZoneMemberCountry;

$zone = new Zone();
$zone->setId('german_vat');
$zone->setName('German VAT');
$zone->setScope('tax');
                        

Add Germany to the zone,


$germanyZoneMember = new ZoneMemberCountry();
$germanyZoneMember->setCountryCode('DE');
$zone->addMember($germanyZoneMember);
                        

add the 4 Austrian postal codes that are in Germany for VAT.


$austriaZoneMember = new ZoneMemberCountry();
$austriaZoneMember->setCountryCode('AT');
$austriaZoneMember->setIncludedPostalCodes('6691, 6991:6993');
$zone->addMember($austriaZoneMember);
                        

Zone matcher

Initialising a zone matcher.


use CommerceGuys\Addressing\Model\Address;
use CommerceGuys\Zone\Matcher\ZoneMatcher;
use CommerceGuys\Zone\Repository\ZoneRepository;

$repository = new ZoneRepository('resources/zone');
$matcher = new ZoneMatcher($repository);
                        

Zone matcher

Create an address.


$austrianAddress = new Address();
$austrianAddress->setCountryCode('AT');
$austrianAddress->setPostalCode('6692');
                        

Get the matching tax zones.


$zones = $matcher->matchAll($austrianAddress, 'tax');
                        

Is in Germany for VAT.

tax

  • Smart data model designed for fluctuating tax rate amounts ("19% -> 21% on January 1st").
  • Predefined tax rates and zones for EU countries, Iceland, Norway, South Africa and Switzerland. More to come.
  • Tax resolvers with logic for all major use cases.

EU Tax Resolver


use CommerceGuys\Tax\Repository\TaxTypeRepository;
use CommerceGuys\Tax\Resolver\Engine\TaxTypeResolverEngine;
use CommerceGuys\Tax\Resolver\Engine\TaxRateResolverEngine;
use CommerceGuys\Tax\Resolver\TaxType\EuTaxTypeResolver;
use CommerceGuys\Tax\Resolver\TaxRate\DefaultTaxRateResolver;
use CommerceGuys\Tax\Resolver\TaxResolver;

$taxTypeRepository = new TaxTypeRepository();
$taxTypeResolverEngine = new TaxTypeResolverEngine();
$taxTypeResolverEngine->add(new EuTaxTypeResolver($taxTypeRepository));
$taxRateResolverEngine = new TaxRateResolverEngine();
$taxRateResolverEngine->add(new DefaultTaxRateResolver());
$resolver = new TaxResolver($taxTypeResolverEngine, $taxRateResolverEngine);
                        

Calculates the VAT rate based on

  • Goods or Services
  • Type of service, digital, education
  • B2C or B2B
  • Supplier location
  • Customer Location
  • Date

Full Data inc. Historic Rates


{
"name": "Romanian VAT",
"zone": "ro_vat",
"tag": "EU",
"rates": [
    {
    "id": "ro_vat_standard",
    "name": "Standard",
    "display_name": "% VAT",
    "default": true,
    "amounts": [
      {
        "id": "ro_vat_standard_2007",
        "amount": 0.19,
        "start_date": "2007-01-01",
        "end_date": "2010-06-30"
      },
      {
        "id": "ro_vat_standard_2010",
        "amount": 0.24,
        "start_date": "2010-07-01"
      }
                        

We're doing something right...

Drupal Commerce 2.x

Once again, we start from scratch


<?php

namespace Drupal\commerce\Entity
                        

2.x Entity Relationship Model

Multi-store / Multi-vendor

Hierarchical Product Model

Improved Order Workflows

Payments

// no code yet.
  • Improve Management
  • Concept of Modes: Live/Sandbox
  • Improve Payment Processing.
    - Support for:
    Preauthorizations / Refunds / IPN / Tokens
    - Standardized statuses

Checkout

// no code yet.

UX improvements

  • Optimize forms experience
  • Support Anonymous
  • Checkout Progress
  • Addressbook

Try it out

github.com/commerceguys/commerce

Contribute on GitHub

Contribute!

Office hours: Wednesdays at 3PM GMT + 2
Find us in #drupal-commerce.