Create full Drupal Commerce orders programatically via the DC API including payment, addresses and more

Drupal Commerce (commerce.module) is a great extension to Drupal CMS (drupal.module) for full functionaly eCommerce online shop functionality. Less for a standard products shop (where other more specialized eCommerce systems like OXID, Shopware, Magento or others come in), but expecially for very custom shops with for example user-customizable products.

We've already implemented several custom product shops for our German customers. In some cases we also needed a functionality to create orders from code programatically.
That's useful for example, if you import old orders, already have a list of predefined customer orders and other usecases.

It was always a horror to find out the right way to implement this, because it's not fully described by examples or documentation yet. There are many things you can do wrong and still I'm not sure if there are people who will help me to make the process even better...

So I decided to provide my implementation to you to make it easier for others to implement an order generation from code. The following code shows all steps from an existing user (that's what I assume) to the full order containing addresses, a payment method and that are ready for final checkout.
The only thing that's not yet included is the shipping method, because I've just had to handle digital products with no shipping. Feel free to add an implemantation for that.

I hope it will help you and please provide some feedback or even better implementations for your order generation.

Talked enough, here's the code:

  1. <?php
  2.  
  3. /**
  4.  * Creates an order for the given user containing the product with the given id.
  5.  * This example may be extended easily to create an order containing several
  6.  * products, a shipping and so far.
  7.  *
  8.  * Returns a commerce order that's now saved in the database with the status
  9.  * "processing and can be finished by an administrator in the UI.
  10.  * Throws an exception, if something is wrong.
  11.  *
  12.  * @param integer $uid The numeric user id, e.g. "1"
  13.  * @param integer $product_id The numeric product id (NOT SKU! ;))
  14.  * @return CommerceORder
  15.  * @throws Exception
  16.  */
  17. function my_module_api_create_order($uid, $product_id) {
  18.   if (empty($uid) || !is_numeric($uid)) {
  19.     throw new Exception('User ID may not be empty and must be numeric!');
  20.   }
  21.   $user = user_load($uid);
  22.  
  23.   if (empty($product_id) || !is_numeric($product_id)) {
  24.     throw new Exception('Product ID may not be empty and must be numeric!');
  25.   }
  26.  
  27.   // Use a transaction to ensure that no broken order is created!
  28.   $transaction = db_transaction();
  29.   try {
  30.     // Create the new order in checkout; you might also check first to
  31.     // see if your user already has an order to use instead of a new one.
  32.     $order = commerce_order_new($user->uid, 'checkout_checkout');
  33.  
  34.     // Save the order to get its ID.
  35.     commerce_order_save($order);
  36.     // Get the order wrapper which provides helper functionality
  37.     $order_wrapper = entity_metadata_wrapper('commerce_order', $order);
  38.  
  39.     // Add address profiles.
  40.     // IMPORTANT! The address MUST be set before the line item to
  41.     // correctly determine the country of supply.
  42.     // IMPORTANT2: This address setup is made based on single fields in the user
  43.     // account. If you already use an address field in the user account
  44.     // this is much simpler and you should have a look at the
  45.     // commerce_extra module for address fields ;)
  46.     // Billing
  47.     // Check that we have at least all REQUIRED fields!
  48.     if (empty($user->field_firstname[LANGUAGE_NONE][0]['value'])) {
  49.       throw new Exception('field_firstname may not be empty!');
  50.     }
  51.     if (empty($user->field_lastname[LANGUAGE_NONE][0]['value'])) {
  52.       throw new Exception('field_lastname may not be empty!');
  53.     }
  54.     if (empty($user->field_city[LANGUAGE_NONE][0]['value'])) {
  55.       throw new Exception('field_city may not be empty!');
  56.     }
  57.     if (empty($user->field_zip[LANGUAGE_NONE][0]['value'])) {
  58.       throw new Exception('field_zip may not be empty!');
  59.     }
  60.     if (empty($user->field_street[LANGUAGE_NONE][0]['value'])) {
  61.       throw new Exception('field_street may not be empty!');
  62.     }
  63.     if (empty($user->field_country[LANGUAGE_NONE][0]['value'])) {
  64.       throw new Exception('field_country may not be empty!');
  65.     }
  66.     // We have to get or create the first order profile of the users to set the values into.
  67.     // We use a helper function for that!
  68.     $billing = _my_module_api_create_order_get_first_customer_profile('billing', $user);
  69.     $customer_profile_billing_array = array(
  70.       'country' => isset($user->field_country[LANGUAGE_NONE][0]['value']) ? $user->field_country[LANGUAGE_NONE][0]['value'] : NULL, // DE
  71.       'name_line' => NULL, // Testperson
  72.       'first_name' => isset($user->field_firstname[LANGUAGE_NONE][0]['value']) ? $user->field_firstname[LANGUAGE_NONE][0]['value'] : NULL, // Max
  73.       'last_name' => isset($user->field_lastname[LANGUAGE_NONE][0]['value']) ? $user->field_lastname[LANGUAGE_NONE][0]['value'] : NULL, // Mustermann
  74.       'organisation_name' => isset($user->field_firma[LANGUAGE_NONE][0]['value']) ? $user->field_firma[LANGUAGE_NONE][0]['value'] : NULL, // Muster AG
  75.       'administrative_area' => NULL,
  76.       'sub_administrative_area' => NULL,
  77.       'locality' => isset($user->field_wohnort[LANGUAGE_NONE][0]['value']) ? $user->field_wohnort[LANGUAGE_NONE][0]['value'] : NULL, // Musterstadt
  78.       'dependent_locality' => NULL,
  79.       'postal_code' => isset($user->field_zip[LANGUAGE_NONE][0]['value']) ? $user->field_zip[LANGUAGE_NONE][0]['value'] : NULL, // 32457
  80.       'thoroughfare' => isset($user->field_street[LANGUAGE_NONE][0]['value']) ? $user->field_street[LANGUAGE_NONE][0]['value'] : NULL, // Musterstraße 10
  81.       'premise' => NULL,
  82.       'sub_premise' => NULL,
  83.       'data' => NULL,
  84.     );
  85.     // Set the values in the profile entity and attache it to the order
  86.     $billing->commerce_customer_address[LANGUAGE_NONE][0] = $customer_profile_billing_array;
  87.     commerce_customer_profile_save($billing);
  88.     $order->commerce_customer_billing[LANGUAGE_NONE][0]['profile_id'] = $billing->profile_id;
  89.  
  90.     // This is only required if you use the commerce_vat module and you want to
  91.     // set the vat id programmatically with implications on the price calculation!
  92.     if (!empty($user->field_vat_number[LANGUAGE_NONE][0]['value'])) {
  93.       $eu_vat_rc = _my_module_api_create_order_get_first_customer_profile('eu_vat_rc', $user);
  94.       $customer_profile_vat_array = array(
  95.         'value' => !empty($user->field_vat_number[LANGUAGE_NONE][0]['value']) ? $user->field_vat_number[LANGUAGE_NONE][0]['value'] : NULL,
  96.       );
  97.       $eu_vat_rc->commerce_vat_number[LANGUAGE_NONE][0] = $customer_profile_vat_array;
  98.       commerce_customer_profile_save($eu_vat_rc);
  99.       $order->commerce_customer_eu_vat_rc[LANGUAGE_NONE][0]['profile_id'] = $eu_vat_rc->profile_id;
  100.     }
  101.  
  102.     // IMPORTANT: If we also use shipping address, this would be the
  103.     // place to set it here like we already did in "billing"!
  104.     // Save the order again to save the address attached.
  105.     $order_wrapper->save();
  106.  
  107.     // Load whatever product represents the item the customer will be
  108.     // paying for and create a line item for it.
  109.     $product = commerce_product_load($product_id);
  110.  
  111.     // Create a new line item with multiplicity = 1
  112.     $line_item = commerce_product_line_item_new($product, 1, $order->order_id);
  113.     // You may set line item properties here.
  114.     // Calculate the sell price!
  115.     rules_invoke_event('commerce_product_calculate_sell_price', $line_item);
  116.     // Save the line item
  117.     commerce_line_item_save($line_item);
  118.  
  119.     // Attach the line item to the order using the order wrapper.
  120.     $order_wrapper->commerce_line_items[] = $line_item;
  121.     $order_wrapper->save();
  122.  
  123.     // Now calculate the total price and save the order again.
  124.     commerce_order_calculate_total($order);
  125.     $order_wrapper->save();
  126.  
  127.     // To create the payment we need some information from the order:
  128.     $total = $order_wrapper->commerce_order_total->amount->value();
  129.     $currency_code = $order_wrapper->commerce_order_total->currency_code->value();
  130.     $charge = array(
  131.       'amount' => $total,
  132.       'currency_code' => $currency_code,
  133.     );
  134.  
  135.     // The following is an example for payment methods commerce_directdebit and bank_transfer.
  136.     // You may use this as example for other payment types!
  137.     // SEPA if we have the required users direct debit fields, otherwise bank transfer!
  138.     if (!empty($user->field_accountholder[LANGUAGE_NONE][0]['value']) && !empty($user->field_bic[LANGUAGE_NONE][0]['value']) && !empty($user->field_iban[LANGUAGE_NONE][0]['value'])) {
  139.       // SEPA
  140.       $payment_method = array(
  141.         'instance_id' => 'commerce_directdebit|commerce_payment_commerce_directdebit',
  142.       );
  143.       commerce_directdebit_transaction($payment_method, $order, $charge, $user->field_accountholder[LANGUAGE_NONE][0]['value'], '', '', 1, $user->field_bic[LANGUAGE_NONE][0]['value'], $user->field_iban[LANGUAGE_NONE][0]['value']);
  144.     }
  145.     else {
  146.       // BANK TRANSFER
  147.       $payment_method = array(
  148.         'instance_id' => 'bank_transfer|commerce_payment_bank_transfer',
  149.       );
  150.       commerce_bank_transfer_transaction($payment_method, $order, $charge);
  151.     }
  152.     // Set the payment id in the order array to make it available for invoicing
  153.     // (no idea why we have to do that manually here. Perhaps there's a better way?)
  154.     $order->data['payment_method'] = $payment_method['instance_id'];
  155.     $order_wrapper->save();
  156.  
  157.     // Update the status to processing to allow manual finishing later on.
  158.     commerce_order_status_update($order, 'processing');
  159.  
  160.     // No explicit transaction commit wanted by drupal...
  161.   } catch (Exception $e) {
  162.     // Something went wrong! We don't want to save a broken order to the database
  163.     // at all. So let's roll back the transaction:
  164.     $transaction->rollback();
  165.     // Add some information to the exception for easier debugging in an outer try / catch!
  166.     $e->user_id = $uid;
  167.     $e->product_id = $product_id;
  168.     // We've done what we could. Re-throw the exception and let an outer
  169.     // try/catch handle it for error logging.
  170.     throw $e;
  171.   }
  172.  
  173.   // Yeah, the order was created successfully. Provide it to the outer call! :)
  174.   return $order;
  175. }
  176.  
  177. /**
  178.  * Helper function to get the users already existing (as implemented: first)
  179.  * commerce customer profile and return it.
  180.  * If the user has no customer profile yet, create a new one and return that.
  181.  *
  182.  * @param string $type The type of the customer profile e.g. "billing".
  183.  * @param stdClass $user The user object.
  184.  * @return stdClass The CommerceCustomerProfile object.
  185.  */
  186. function _my_module_api_create_order_get_first_customer_profile($type, stdClass $user) {
  187.   $query = new EntityFieldQuery();
  188.   $query->entityCondition('entity_type', 'commerce_customer_profile')
  189.       ->propertyCondition('uid', $user->uid)
  190.       ->propertyCondition('type', $type);
  191.  
  192.   $results = $query->execute();
  193.   if (!empty($results['commerce_customer_profile'])) {
  194.     // Profile already exists. Load!
  195.     $profile_info = reset($results['commerce_customer_profile']);
  196.     return commerce_customer_profile_load($profile_info->profile_id);
  197.   }
  198.   else {
  199.     // No profile yet. Create one!
  200.     return commerce_customer_profile_new($type, $user->uid);
  201.   }
  202. }

Finally some links that helped me to find out, how it works:

Legal note: The provided code is not intended for production sites! This is just a development example. No liability, no warranties!

AnhangGröße
drupal_commerce_order_creation_by_api_example.php_.txt9.74 KB

Kommentare

Bild des Benutzers Pedro

THANK YOU!! THANK YOU!! THANK

THANK YOU!! THANK YOU!!
THANK YOU!!
THANK YOU!!
THANK YOU!!
THANK YOU!!

You just saved my project!

Kommentar hinzufügen

Der Inhalt dieses Feldes wird nicht öffentlich zugänglich angezeigt. Wenn Sie ein zulässiges Avatar mit Ihrer E-Mail-Adresse verknüpft haben, wird dieses als Avatar verwendet.

Weitere Informationen über Formatierungsoptionen

Aktualisieren Geben Sie die Zeichen ein, die Sie im Bild sehen. Geben Sie die im Bild dargestellten Zeichen ein; wenn Sie diese nicht lesen können, senden Sie das Formular ohne Eingabe ab, um ein neues Bild zu generieren. Groß-/Kleinschreibung wird nicht beachtet.  Switch to audio verification.