By Senior Magento Developer Alan Barber
This week my team celebrated the completion of our most recent project: TeachMeToday.com. This is a heavily customized version of Magento which allows users the opportunity to pay for a membership which gives them access to over 400 eLearning courses.
- We developed the following custom functionality
- Category and Product Import : TeachMeToday’s catalog was imported entirely from a 3rd party provider. We developed an extension to import a category hiearchy and assign products to these categories based on the external catalog. We also created a script to perform daily synching between the two catalog versions
- Private Sales Functionality : We modified Magento so that a user must pay for a membership before they can view any of the eLearning courses
- Recurring Memberships (Subscriptions) : We developed a custom extension which allows TeachMeToday to charge their active members on a monthly basis using Authorize.net’s CIM
- Affiliate Landing Pages with Custom Checkout : My team integrated an external checkout path with TeachMeToday’s Magento installation. Essentially, affiliate traffic signs up on a highly optimized version of the Magento checkout (which is normally very clunky)
- Affiliate Tracking / Reporting : We added some functionality to the Magento success page and admin area that tracks conversions and rejections
Each of these areas could have a full article written about them, but I will try and provide a few paragraphs about how we accomplished each
3rd Party Category and Product Import
This was probably the largest piece of the project. TeachMeToday had ~400 products and ~80 categories in an external catalog. Each of the products was essential an online “course” which launched a java application on the user’s computer. While we couldn’t import the java apps themselves, we could import the category hierarchy and metadata for individual courses. The process we developed for this is as follows:
- Create your module : We created a custom module in app/code/local.
- Add a sql update script to track custom data : Inside our module’s SQL directory we added an install script to add a custom attribute to both products and categories, which would serve to hold a serialized object representing metadata from our 3rd party catalog
- Add a helper to connect with the API : our 3rd party catalog provided a soap API to retrieve data. We created an API helper in our module which extended the php SoapClass. We used PHP’s magic functionsto allow the rest of our module to access API methods in the form of
Mage::helper("apihelper")->{api_method_name}
— cool huh? The the other objects in the module, the API calls were just regular method calls…our soap class had built in functions to handle soap faults as errors and to parse the soap response.
- Get a parsable version of the external catalog : In our case, this was a giant XML tree of the entire catalog:
- Recursively walk through the tree and update the catalog: This is a big step. A few things were invloved:
- I wrote models to represent the different types of nodes in the xml tree : In the external catalog, categories were tagged as “groups” and products tagged as “assets”. I wrote some models which extracted relevent data from each of these node types
- I wrote a model to represent the tree as a whole : We needed a model to poll the 3rd party service, retrieve the tree as xml, parse it into php SimpleXML and then recursively walk down each branch of the tree. Depending on the current node’s type, a different operations was performed
- I wrote helper models to map the xml nodes into Magento models : Helper were created which took in a <group> and mapped it into a category (in the appropriate place in the category tree) and took an <asset> tag and mapped it into a product (and assigned it to the correct category). The helpers were also smart enough to make a distinction between: creating a new model and updating an existing model
- Handle errors : Seems simple, but you ABSOLUTELY NEED functions built in to methodically log and report errors. We could not have done this process without such logic. Magentohas a wonderful function:
Mage::log({message}, null, {filename})
which allows you to put a custom log at var/log/{filename}
- Setup a cron job to recursively walk through the tree every night : In case the 3rd party catalog changed, we wanted our catalog to represent those changes. So we setup a cron in our config.xml to update the tree every night.
Private Sales Functionality
TeachMeToday is based on a membership which gives users access to all products in the system. Be default, the community edition of Magento doesn’t enable this functionality. However it is surprisingly simple to implement this:
- Rewrite the customer account controller in your module’s config.xml
- Override the creatAction() in AccountController.php : In our case, we overwrote the create action to redirect the user to one of our landing pages so they could sign up. In this way, a user cannot navigate to /customer/account/create and get a new account…no matter how many times they try, they will alows get redirected to our custom landing pages
- Remove the “launch course” option for user’s that aren’t active in the system: All of TeachMeToday’s products are virtual courses, and as part of the private sales piece, we needed to make sure a user that didn’t have a membership (or had an expired membership) could not launch the courses. We created a helper that checks a special customer attribute which is a Boolean flag specifying whether or not the user was active. So we use
Mage::getSingleton("customer/session")->getCustomer()
to grab the current customer instance and check their is_active flag.
- Update the customer flag when a user signs up for a membership: We created a custom event for this. On our landing pages (which have custom checkout functionality) we call
Mage::dispatchEvent("our_custom_event", array("customer" => $customer))
which passes the newly-signed-up customer to our custom module (which then updates the appropriate customer attributes). We created a similar event for when the customer is deleted or fails billing.
Recurring Memberships (Subscriptions)
We needed a way to continually charge customers (since the product they were purchasing was a monthly membership/subscription). However, it requires a lot of legwork and special infrastructure to store credit cards with and be PCI Compliant — and the community edition of Magento is not PCI compliant. Therefore, we decided to use Authorize.net’s CIM. CIM allows you as merchant to store credit card information on authorize.net’s servers, and then provides you a handle for each customer with which you can rebill them without storing their info — cool huh?
Our process was as follows:
- Create the CIM customer and payment profiles when the user signs up, and store the CIM token (handle) : We used the IDP Magento Extension to handle the requests to authorize.net (no point in reinventing the wheel, and this module does a great job creating a Magento Paygate-mapping to authorize.net).
- Create a cron task to bill customers: My team created a script (which runs nightly) that does a few things:
- Check which customers are due for a rebill : Using varien data collections we find a list of all customers which are due for a re-bill
- Attempt to rebill appropriate customers through their CIM token : try and bill the customer through their CIM handle (which bills them based on the credit card info they have stored in authorize.net)
- If payment failed, change their “is_active” flag, if not, create a new order
Magento Affiliate Landing Pages with Custom Checkout
>We needed a number of different landing pages for TeachMeToday to AB test affiliate traffic to. These landing pages needed to be highly optimized for conversions. The process for creating landing pages that could create orders in Magento was as follows:
Create CMS Pages with each landing page URL
- : If you wanted your landing page to be: TeachMeToday.com/signup-now, you would make a cms page with URL identifier of “signup-now”
- Set the CMS page layout to empty
- Add custom stylesheets via layout xml in cms_page–>design : If you need custom styles, you can add them on a per landing page basis there
- Create a phtml block in the CMS page to place your landing page code in : We had vastly different looking landing pages…which were all their own layout. The way I did this was to place a block as the sole content in each CMS page as: {{block type=”page/html” name=”signup.now.page” as = “signupNowPage” template=”landing/signup_now/body.phtml”}} Now the page would load its content from my custom phtml file:
- Create your custom checkout controller that your landing page submits to : This step is a pain in the @!@ … essentially you need to replicate everything Magentodoes when it creates an order, which is as follows:
- Validate customer billing info
- Create a new customer with an auto-generated password
- Create a quote model and load it with customer and product data : You will need to assign the quote to a customer and add a product to the quote. I’m not going to go into detail on how to do it as its a complicated process. Googling for “create a Magento order programatically” returns some relevent results
- Use a service quote to attempt to transform the quote into an order (and therefore charge the customer’s credit card)
- If the service quote threw an error, notify the user their payment info was invalid. Otherwise, get the new order from the service quote and move the user to /checkout/onepage/success
Magento Affiliate Tracking / Reporting
Since TeachMeToday receives a large amount of affiliate traffic, they needed to do a few things:
- Store email addresses of leads: Even if a user didn’t not checkout, we needed to store all the email addresses that came through. We did this by associating everyone who comes through the site as a newsletter subscriber (
Mage::getModel("newsletter/subscriber")
)
- Track conversions: Magento by default has a .phtml file in template/checkout/onepage/success.phtml that gets loaded on the checkout confirmation page. This is a great place to drop in tracking pixels or javascript for affiliate tracking (most affiliates will require their tracking pixel on the confirmation page). We passed in the current affiliate’s id in the url string to the confirmation page (as /checkout/onepage/success?aid={affiliate_id}). We then logged every order that had an affiliate id set in a custom table in our database (calling
Mage::getSingleton("checkout/session")->getLastRealOrderId() will give you the order id).
- Create reports in the back end of Magento : I’m not going to go into how to add reports to the adminhtml. However, we used the aforementioned table that we stored aid=>order_id associations in to populate several custom reports for each affiliate
Conclusion
I’ve outlined a lot of complicated processes above. It was a considerable amount of work—but it goes to show how Magento’s modular structure allows you to do just about anything with it (and how awesome our Customer Paradigm Team is!).
Alan Barber is a Senior Magento Programmer at Customer Paradigm who specializes in systems architecture and application troubleshooting. For more information, or to get Magento help now, visit customerparadigm.com.