Skip to content

Instantly share code, notes, and snippets.

@unicoder88
Last active February 11, 2020 06:43
Show Gist options
  • Star 5 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save unicoder88/4cb771f44da42e89595fb1c1b05efa46 to your computer and use it in GitHub Desktop.
Save unicoder88/4cb771f44da42e89595fb1c1b05efa46 to your computer and use it in GitHub Desktop.

DEVELOPER PLUS

87 Multiple Choice items (85 scored, 2 unscored)
120 minutes to complete the exam
Based on Magento Community Edition 1.9 and Magento Enterprise Edition 1.14

Passing score

Magento Certified Developer Plus exam: For sections 11 & 12 combined a score of 7 or higher AND meet the overall passing score of 48 or higher

 5%  1-Basics (config, events, cron, translations, theme, locating template/layout file)
 6%  2-Request Flow
 6%  3-Rendering
11%  4-Working with Database
 8%  5-Entity-Attribute-Value (EAV) Model
 6%  6-Adminhtml (ACL, cache, grids, forms, system config)
 8%  7-Catalog (product types, indexing, prices, categories, catalog rules, tax)
13%  8-Checkout (inventory, add to cart, totals, cart rules, payment methods, shipping methods, multishipping)
 9% 9-Sales and Customers (order, refund, partial invoice/shipping/refund, cancel, customers)
11% 10-Advanced features (widgets, API)
11% 11-Enterprise Edition (target rules, reward points, website restrictions, full page cache, payment bridge)
 8% 12-Challenge Questions

Mage

run:

  • events collection
  • app request, response (optional)
  • config
  • run app

Mage_Core_Model_App

baseInit:

  • error handler + default timezone
  • load base config - /etc/*.xml (config.xml, local.xml)
  • cache - new core/cache by , request_processors x send response here if in cache _initModules:
  • loadModules (declared, modulesConfiguration)
  • applyAllUpdates (unless new install)
  • loadDb
  • saveCache x code and db are ready run:
  • load area (global events)
  • initCurrentStore (cookie,get) |
  • initRequest (pathinfo) | unless new install
  • applyAllDataUpdates |
  • front dispatch

Mage_Core_Controller_Varien_Front

<web>
    <routers>
        <admin>
            <area>admin</area>
            <class>Mage_Core_Controller_Varien_Router_Admin</class>

x <web><routers> - admin, frontend. CollectRoutes from <admin><routers> and <frontend><routers> init:

  • events controller_front_init_before -- prepend routers here.
  • routers (from <config><web><routers>) + collectRoutes ([frontName] => modules[]) router.collectRoutes(router_key, router_area)
    • router key -- only routes with <use> == router_area are collected. e.g. use=admin, use=standard
    • router area -- load routes from area, e.g. frontend/routers, admin/routers ! admin/routers -- registers concrete route . in modules reference concrete route to add more controllers to chain
  • event controller_front_init_routers -- append routers here. CMS adds router here

dispatch:

  • _checkBaseUrl (unless admin)
  • rewrite (db + config)
  • loop
  • send response (headers, exceptions, body)
  • events (2)

Mage_Core_Controller_Varien_Action

dispatch:

  • preDispatch
  • call action
  • postDispatch preDispatch:
  • rewrite (+forward)
  • cookie + session start
  • load area (config, events, design, translate)
  • events (3) postDispatch:
  • events (3)

Mage_Core_Model_App_Area

  • app addEventArea
  • translator init (setConfig: locale code, store id, design package name (exceptions: regexp user agent -> custom package, skin, template, layout, default)) initDesign (only front):
  • design = design package singleton
    • design config (/app/design/{area}/{package}/{theme}/etc/theme.xml -> tree {area}/{package}/{theme}/...) ! theme.xml: layout/updates here
    • design fallback (inheritance: parent = package/theme, legacy: theme default, 'default')
  • design_change (store, date): package/theme -> design package packageName, theme

Design Package

getLayoutFilename, getTemplateFilename, getLocaleFilename -> getFilename

getFilename:

  • getFallbackScheme - first no update, then parents
  • _fallback(file, params, fallback themes)
  • return found validateFile: getBaseDir.file
  • or renderFilename base/default

storeConfig:

  • design/package/name
  • design/theme/default

Translate

translate_inline - allowed and active admin/front - only devAllowed

LoadLayout(handles=default)

  • update addHandle
  • addActionLayoutHandles STORE_$code, THEME_$area_$package_$layout, $route_$controller_$action

action loadLayoutUpdates:

  • event
  • update load update load:
  • loadCache
  • merge[]
  • saveCache fetchFileLayoutUpdates:
  • getFileLayoutUpdatesXml (once) area, package, theme, store id - load all layout files
    • event
    • update files (config $area/layout/updates, theme layout/updates, local.xml) ! frontend/layout/updates/(convert_shopby module="Convert_Shopby" - to disable output)/file='convert/shopby.xml'
    • append all layout files xmls merge[] handle: builds _updates array of all matched handle contents
  • fetchPackageLayoutUpdates (handle)
    • packageLayout handle
      • fetchRecursiveUpdates + addUpdate handle text content
  • fetchDbLayoutUpdates (handle)
    • getUpdateString handle from core_layout_update, core_layout_link by handle, store id, area, package, theme
    • fetchRecursiveUpdates + addUpdate

action generateLayoutXml:

  • event
  • layout generateXml
    • - ignore block and its references (but keep if acl isAllowed)
    • setXml

action generateLayoutBlocks:

  • events (2)
  • layout generateBlocks (recursive)
    • ignore marked
    • generateBlock
      • type, name -> createBlock, _prepareLayout, event
      • insert before/after/append parent->child
        • insert anonymous no suffix - _blocks[ANONYMOUS_0]->_blocks[root.child0], nameInLayout=root.child0, alias=root.child0, _children[root.child0]
        • insert anonymous with suffix _blocks[ANONYMOUS_1]->_blocks[root.suffix], nameinLayout=root.suffix, alias=root.suffix, _children[root.suffix]
        • insert name no suffix _blocks[head], alias=head, _children[head]
        • insert name with suffix _blocks[footer.before], _children[footer_before]
      • template, output
      - nameInLayout = alias = parent.child0 - nameInLayout = alias = parent.suffix
    • generateAction <arg1_array> value0 value1 </arg1_array> true false false Translatable string _blocks[root].doSomething( array(key1 => value1, key2 => value2), // simple assoc helper(catalog/category)->getStoreCategories(true, false, false), // helper result json_decode(helper(module)->returnJson()) array(b => array(c => helper(core)->__(Translatable string))) );

layout xml: simplexml_element layout simplexml_element block name=root simplexml_element block name=head ... simplexml_element block name=core_profiler

_blocks[root] = block_object(type=page/html,nameInLayout=root,layout=obj,template=page/3columns.phtml,_children=[obj]) _blocks[head] = block_object(type=page/html_head,nameInLayout=head,layout=obj,parentBlock=obj,blockAlias=head) ... _output[root] = toHtml

renderLayout

  • renderTitles - $this->_title('foo')->_title('bar') => head: foo / bar
  • events (2)
  • layout getOutput
  • translate inline { {{original}} {{.}} {{.}} {{.*}} }
  • response appendBody

send response events: Mage_Core_Controller_Varien_Front::dispatch - 2 events Mage_Core_Controller_Response_Http::sendResponse - event

Block page/html_head

items[type/name], type - js_css, skin_css, js, skin_js; rss, link_rel

addCss(styles.css) addJsCss(js-slider/slider.css) addItem(skin_css, lucky.css, 'whatsthis?', 'lt IE 7', 'feel_lucky') addItem(rss, http://feed.mikle.com/support/rss/, 'title=RSS') addLinkRel(http://non.skin/style.css, 'rel=stylesheet type=text/css')

_items["skin_css/styles.css"] = [type skin_css, name styles.css] _items["js_css/js-slider/slider.css"] = [type js_css, name js-slider/slider.css] _items["skin_css/lucky.css"] = [type skin_css, name lucky.css, params 'whatsthis?', if 'lt IE 7', cond 'feel_lucky'] _items["rss/http://feed.mikle.com/support/rss/"] = [type rss, name http://feed.mikle.com/support/rss/, params "title=RSS"] _items["link_rel/http://non.skin/style.css"] = [type link_rel, name http://non.skin/style.css, params "rel=stylesheet type=text/css"]

getCssJsHtml: lines['']['skin_css']['']['styles.css'] = 'styles.css' lines['']['js_css']['']['js-slider/slider.css'] = 'js-slider/slider.css' if $feel_lucky: lines['lt IE 7']['skin_css']['whatsthis?']['lucky.css'] = 'lucky.css' lines['']['other'][] = lines['']['other'][] =

prepare css (+merge), prepare js (+merge), prepare other _prepareStaticAndSkinElements:

  • designPackage getSkinUrl or getFilename
  • if merged, single file per group, else separate files

Block Abstract

caching, parent/children, sort, child groups, messages block, surround frame, helpers (url, date, escape), inline translate

  • set layout events (2)
  • to html events (2) absolute path - Mage::getDir('design') -> core/config_options::getDir() -> getDesignDir()

DB

model - define resource model when constructed resource model (entity) - define resource prefix (for read/write connections) / main table entity init(main table, id field). mymodule/maintable: resource prefix = mymodule, resource model = maintable e.g. connections = {resourcePrefix}{connection} = mymodule_read, mymodule_write collection - define model and resource model when constructed migrations: all resources with setup child -> module name -> module/resource_name/migrations...

Resource Setup

install/upgrade/rollback/uninstall, php/sql install-{$version}, upgrade-{$version} {$connection->model}-install-{$version}, mysql4-upgrade-{$version}

class core/resource_setup utility methods:

  • run (multi sql)
  • getTable, tableExists
  • getTableRow, deleteTableRow, updateTableRow
  • setConfigData, deleteConfigData
  • getIdxName, getFkName

core/resource_setup_query_modifier

Mage_Eav_Model_Entity_Setup

Db Adapter

transactions tables - create, drop, describe, ... + batch columns - add, modify, drop, ... indeces foreign keys select - Varien_Db_Select inserts - 5 methods update, delete query, multiQuery fetch - 6 methods quote - 5 methods startSetup, endSetup dql cache - 6 methods prepare condition, value sql fragments - check, case, ifnull, concat, length, least, greatest, substring, std. deviation date format, add, sub, to/from unix timestamp getTableName generate name - index, fk enable/disable keys (indexing) selects by range insert/update/delete from select table checksum order rand for update primary key name decode binary create table from select drop trigger change autoincrement

Resource

  • getModelName name or [name, suffix] + event

Model

getResourceModel(convert_acme/flat) => resourceModel = global/models/convert_acme/resourceModel, getModel(resourceModel/flat) load:

  • beforeLoad + events (2): generic, tailored (_eventPrefix, _getEventData, _eventObject)
  • resource load
  • afterLoad + events (2) generic, tailored
  • set orig data, mark no changes save:
  • delete if isDeleted
  • check _hasModelChanged
  • resource.begin transaction
  • before save (can _dataSaveAllowed), set is new + events (2) model_save_before, {prefix}_save_before
  • resource save
  • after save - cleanModelCache + events (2)
  • resource commit
  • afterCommitCallback + events (2) model_save_commit_after, {prefix}_save_commit_after

Model Resource

Mage_Core_Model_Resource_Abstract:

  • commit, rollback
  • commit callbacks
  • format date
  • serialize/unserialize field
  • get read/write adapter
  • _prepareDataForTable -- keeps only data for columns that exist in table

flat - Mage_Core_Model_Resource_Db_Abstract:

    • serializableFields, uniqueFields, ...
  • getTable name or [name, suffix], name can be full model/entity or short entity (current model)
  • getMainTable
  • load:
    • getLoadSelect, fetchRow, setData
    • unserializeFields
    • afterLoad
  • save:
    • delete if isDeleted
    • serialize fields
    • before save
    • check unique
    • detect mode update/insert (model getId() and optionally isObjectNew())
      • have id, not new - normally update, but if no auto-increment decide insert/update using select id exists (explicit id)
      • no id or is new - insert, set last insert id
    • unserialize
    • after save

_getLoadSelect: getReadAdapter()->select() = Varien_Db_Adapter_Pdo_Mysql->select() = new Varien_Db_Select connection instanceof Zend_Db_Adapter_Abstract Zend_Db_Adapter_Abstract::select() - new Zend_Db_Select

eav/resource_entity_abstract - eav

Group Save

core/resource_transaction: - order, invoice, creditmemo, shipment, quote submitOrder

  • addObject, addObject, ... - models with getResource method
  • addCommitCallbacksave, addCommitCallback, ...
  • save - save[], callback, commit
  • delete - delete[], callback, commit Varien_Db_Adapter_Interface: insertMultiple, insertArray (model->getResource() | Mage::getSingleton('core/resource'))->getConnection('default_write')

Database-related classes

Collection

Varien_Data_Collection:

  • ! add/get filter
  • curPage, pageSize
  • ! count - loaded items, getSize - all collection, get last page number count($collection) = load() + count(items) $collection->getSize() = smart COUNT() SQL
  • get items/first/last/by id
  • get column values/items by column
  • add item, remove item by key
  • get all ids
  • load, load data, clear
  • walk, each
  • set data to all
  • set order
  • set item object class, get new empty item
  • to xml/array/option array/option hash
  • cache key/tags/lifetime
  • distinct
  • render filters/orders/limit

filtering

addFilter('name', 'Roman', 'and') # noop setOrder('position', 'asc')

Varien_Data_Collection_DB:

  • connection (Zend_Db_Adapter_Abstract)
  • select (Zend_Db_Select), get select sql,
  • add bind param
  • id field name
  • init cache (external cacher) - prefix, hash select
  • ! get cur page - loads collection! fixes out of bounds
  • ! get select count sql - used by getSize
  • order
  • ! add field to filter
  • fetch item
  • get/reset data
  • add filter to map
  • print log query

filtering

addFilter('name', 'Roman', 'and') # select->where addFilter('name', 'Roman', 'or') # select->orWhere addFilter('', 'stores.id is not null', 'string') # value as is addFilter('name', ['eq' => 'Roman'], 'public')

addFieldToFilter('name', 'Roman') ! addFieldToFilter('name', ['eq' => 'Roman']) -- one of: eq, neq, like, nlike, in, nin, is, notnull, null, gt, lt, gteq, ... ! addFieldToFilter('name', [ -- nested OR ['eq' => 'Roman'], // OR ['null' => 'anyvalue'], ]) addFieldToFilter(['first_name', 'email', 'roman_friend'], ['Roman', 'roman@example.com', ['notnull' => 'anyvalue']]) # OR

see \Varien_Db_Adapter_Pdo_Mysql::prepareSqlCondition

  • from,to
  • one of: eq, neq, like, nlike, in, nin, is, notnull, null, gt, lt, gteq, lteq, finset, regexp, from, to, seq, sneq
  • nested OR(queries[])

addOrder('position', 'asc') === setOrder unshiftOrder('position', 'asc')

flat - core/resource_db_collection_abstract (resource):

  • model, resource model
  • main table, get table
  • events (2 + 2) core_collection_abstract_load_before, core_collection_abstract_load_after only if _eventPrefix and _eventObject defined: {prefix}_load_before, {prefix}_load_after
  • ! add/remove field/expression field to select
  • ! join (table, cond, cols). join('some/table', 'a = b', '*'); join(['alias' => 'some/table'], 'a = b', ['alias' => 'col'])
  • reset items data changed
  • save -- each item[].save
  • mage app cache -- initCache must be called to work
  • initial fields to select (id)
filtering/select

addFieldToSelect('count(entity_id)', 'cnt') addExpressionFieldToSelect('cnt', 'COUNT({{entity_id}})', 'entity_id')

flat collection caching:

initCache --> _cacheConf:

  • ['prefix'] -- influences cache id
  • ['tags']
  • ['object'] == _getCacheInstance getData:
  • _renderFilters, _renderOrders, _renderLimit
  • _prepareSelect -- stupid
  • ! _fetchAll: -- try to load cache, if miss - load and save to cache
    • _canUseCache -- enabled in admin AND _cacheConf configured
    • _loadCache -- use _getSelectCacheId -- _cacheConf['prefix'] + md5(select)
    • ...
    • _saveCache -- tags = 'mage', 'collection_data' + _cacheConf[tags] -- describe dependencies to refresh your collection, e.g. category model
  • _afterLoadData

Mage_Eav_Model_Entity_Collection_Abstract

  • addAttributeToSelect -- + joinType
  • addAttributeToFilter
  • addAttributeToSort
  • addEntityTypeToSelect -- dummy?
  • addStaticField
  • addExpressionAttributeToSelect
  • groupByAttribute
  • joinAttribute
  • joinField -- regular table
  • joinTable
  • save / delete
  • importFromArray / exportToArray

Adapter

Magento_Db_Adapter_Pdo_Mysql

  • select batch insert, batch split, float fix

Varien_Db_Adapter_Pdo_Mysql

  • debug, DQL cache, hostinfo, check DDL in transaction, query hook, FK_, delete/update/insert/create from select, dates
  • Varien_DB_Adapter_Interface
  • Varien_Db_Select

Zend_Db_Adapter_Pdo_Mysql

Zend_Db_Adapter_Pdo_Abstract

  • dsn, exec

Zend_Db_Adapter_Abstract

  • connection, profiler, crud, select, quote.
  • Zend_Db_Select

Select

Varien_Db_Select:

  • delete/update/insert from select (adapter)
  • order rand (adapter)
  • exists
  • reset left join

Zend_Db_Select:

  • distinct
  • from('name', '*'), from(['alias' => 'table'], ['alias' => 'col'])
  • union([select1, select2])
  • join/joinInner, joinLeft, joinRight,
  • joinFull = left + right, nulls missing
  • joinCross = cartesian product, no condition
  • joinNatural = same name in both columns, no condition
  • where, orWhere
  • group
  • having, orHaving
  • limit, limitPage
  • getPart, reset(part)
  • order
  • query(mode, [bind])

EAV

Guess:

resource model must provide entity type id ✔, id field ✔ attributes (source/backend/frontend model), system, user-defined

resource model, entity, is different - Mage_Core_Model_Resource_Abstract -> Mage_Eav_Model_Entity_Abstract (✔)

  • load, save, delete methods entity collection - Varien_Data_Collection -> Varien_Data_Collection_Db -> Mage_Eav_Model_Entity_Collection_Abstract (✔)
  • join attribute tables setup class - Mage_Core_Model_Resource_Setup -> Mage_Eav_Model_Entity_Setup (✔)
  • add/update attribute
  • modify attribute values

Entity

  • set/get type
  • get/add attribute
  • unset attributes
  • get attributes by code/id/table
  • is attribute static
  • entity table
  • entity id field
  • validate
  • set new increment id
  • check attribute unique value
  • is partial save/load
  • save attribute
  • get default attributes

Entity Collection

  • add attribute to filter == add field to filter
  • add/remove attribute to select
  • add attribute to sort
  • add entity type to select
  • add static field
  • add expression attribute to select
  • group by attribute
  • join attribute
  • join field
  • join table
  • set page
  • import from array/export to array
  • get loaded ids

ADMINHTML

Zend_Cache_Core:

  • clean
  • remove
  • load
  • test
  • ...

Zend_Cache_Backend_Interface:

  • load - single entry
  • test - single entry
  • save - single entry
  • remove - single key
  • clean - all/by tag

flush magento cache:

  • flushSystem - by tag MAGE - added when saving anything in Magento

flush cache storage:

  • flushAll -> all

_isAllowed - controllers, some blocks (custom check) Mage_Adminhtml_Controller_Action::preDispatch:

  • check form key on post, secret key
  • check _isAllowed
  • set default session locale event adminhtml:controller_action_predispatch -> admin/observer::actionPreDispatchAdmin:
  • check open actions
  • handle post login -> admin/session::login -> admin/user::login -> admin/resource_acl::loadAcl -> redirect on success
  • redirect to adminhtml/index/login otherwise

Load ACL in admin/session

admin/resource_acl::loadAcl acl = admin/acl == Zend_Acl

  1. admin/config::loadAclResources into admin/acl resources -> read all adminhtml, add recursively all children, prepending parent/ path, completes each resource with children Zend_Acl::add('all', parent=null)
  2. load roles from admin_role -> into admin/acl roles
  3. load rules! e.g. acl->_rules->byResourceId->[admin/system/config/sales]->byRoleId->[G31]->allPrivileges->type=DENY

admin/resource_acl -> have resources loaded from config, load roles from db, load rules from db

admin_role - union (roles - parent_id=0, user_id=0; assigned users - parent_id=role, user_id=user) admin_rule join admin_assert - role-access to each resource (allow/deny)

layouts: main.xml, report.xml, catalog.xml, customer.xml, promo.xml

adminhtml.xml

...

Grids

widget_container:

  • buttons (adminhtml/widget_button), area (footer)
  • header text
  • event adminhtml_widget_container_html_before

grid_container:

  • template widget/grid/container.phtml [header][buttons] // [grid]
  • add button (createUrl), back button
  • 'grid' auto child
  • header css class, header width

widget:

  • id
  • current url
  • creates button html
  • global icon

widget_grid:

  • data properties: sortable, var_name_filter, grid_header, use_ajax, additional_javascript, collapsed, js: row_click_callback, checkbox_check_callback, row_init_callback
  • _extensible properties: {headers,filter,pager}Visibility, countTotals, countSubTotals, {varName,default}{Limit,Page,Sort,Dir,Filter}, massActionIdField (id, query_id)
  • extensibleMethods: addColumn, addExportType (url,label), addRssList (url,label), setTotals
  • multipleRows: row item->children
  • template widget/grid.phtml
  • columns
  • limit, page, sort, dir, dir
  • totals, subtotals, rss
  • export types: csv, xml, excel
  • main buttons: reset filter, search
  • prepare mass action
  • collection
  • ajax
  • grid header (concrete)
  • jsObjectName: {id}JsObject = varienGrid
  • filter: prepareCollection: prepare pager, set order, set filter values

widget_grid_massaction

  • use_select_all, use_ajax, error_text, form_field_name
  • addItem: items-actions (label, url, selected, additional, confirm), additional action block (create block by name or from config form-like) -- set new status, assign to customer group
  • get javascript: {jsObjectName} = new varienGridMassaction
  • selected json: params(internal_massaction)=1,2,3,..., items, gridIds, useAjax,

widget_grid_column - not to extend:

  • data properties: type, header, index, renderer, filter, width, align, html_decorators (nobr), frame_callback, editable, sortable, dir, css_class, header_html_property, totals_label, filter_index, filter_condition_callback
  • get renderer - class or by type adminhtml/widget_grid_column_renderer_* by type (text, date, datetime, ...)
  • get filter - false, class or by type
  • column filter or by type adminhtml/widget_grid_column_filter_*
  • css: align, column class, editable
  • get row field + decorators, frame callback?
  • get row export field

widget_grid_column_renderer:

  • column getter
  • render header
  • render + column editable input
  • render export
  • render property (width etc.)
  • render header

widget_grid_column_filter:

  • get condition - for select by current value --> collection.addFieldToFilter(column, condition)
  • _toHtml
  • get escaped value

Forms

widget_container>

block adminhtml/widget_form_container:

  • template widget/form/container.phtml
  • 'form' auto child
  • data: form_action_url
  • prop: _formInitScripts, _formScripts, _objectId (id, sitemap_id) for delete button and url
  • buttons header, footer: back, reset, delete, save
  • form action url

block adminhtml/widget_form:

  • template widget/form.phtml [form_html][form_after]
  • data: dest_element_id, show_global_icon
  • element types
  • Form.set{Element,Fieldset,FieldsetElement}Renderer
  • set fieldset (attributes, to fieldset) - to mass add dynamic eav attributes (customer, category)
  • extensions:
    • prepare form, set form
    • init form values
    • get additional element types
    • get additional element html

eav attribute:

  • data: attribute_code, is_visible
  • source: all options
  • frontend: input type, input renderer class, label, class

Varien_Data_Form:

  • set readonly
  • add type (type, class name)
  • add field (id, type, config, after) - creates from types by name
  • add fieldset
  • add column?
  • add element - simply adds instance
  • toHtml

elements: date, label, reset, textarea, multiselect, submit, radios, select, radio, multiline, text, checkbox, checkboxes, note, file, link, image, imagefile, hidden, button, password, column, gallery, time

System Config

adminhtml/config:

  • get sections
  • get tabs
  • loads modules' system.xml
  • apply extends
  • event adminhtml_init_system_config
  • system.xml/sections, system.xml/tabs
  • acl: system/config/{section}
  • default - first allowed section
MyModule 0 mymodule-class-anything Always translated by mymodule (default adminhtml) 0 something-here adminhtml/system_config_form anothermodule 1 1 1 0 anothermodule 1 1 1 adminhtml/system_config_form_fieldset MyGroup Auto translated comment 1 1 mymodule/model custom_key 1 0 anothermodule 1 1 1 mysection/mygroup/myfield adminhtml/system_config_form_field text Auto translated hint mymodule/model mymodule/model Auto translated tooltip mymodule/block mygroup 1 mygroup,myothergroup,yetanothergroup my-class-plus-shared-and-requires css-class-name 0 mymodule/factory

<frontend_type> - form field types + registered with fieldset in adminhtml system_config_form:: _getAdditionalElementTypes:

  • export
  • import
  • allowspecific
  • image
  • file

blocks: left - adminhtml/system_config_tabs content - adminhtml/system_config_edit .form - adminhtml/system_config_form

adminhtml/config_data:

CATALOG

Product Types

virtual downloadable = virtual + link simple = virtual + weight grouped = add 3 products with 1 click. No variations, separate options, no total qty, no group price, no sku control, separate items in cart bundle = add 1 composite product, consisting of many selected variations. options independent. sku dynamic/fixed, weight dynamic/fixed, price dynamic/fixed. each qty x total qty configurable = add 1 composite product, consisting of 1 selected variation. all options are connected

custom product - create? index? store custom data? existing:

  • calculation?
  • parent-child?
  • shared tables and specific?

model/product.getTypeInstance (singleton|ornot).model/product_type.factory

MyModule Custom Product Type mymodule/product_type_mytype mymodule/product_type_mytype_price 1 1 1 0 mymodule/catalog_product_price_mytype mymodule/catalogindex_data_mytype

salable/available checks: (== enabled and inventory)

  • type instance.isSalable
    • enabled and data(is_salable) <-- by inventory>
    • not composite. why?
  • product.isAvailable = above + ability to skip in admin when create/edit order?
  • product.isSalable = above + 2 events
  • product.getIsSalable -- stupid, doesn't check enabled
    • type instance.getIsSalable
    • data(is_salable) <-- by inventory>
    • fallback above

product:

-- sturdy data: .type_id = 'mytype' .has_options = 1 -- flimsy data: .category_id (product.setCategoryId(something); product.getCategory() -- loads as defined) .category_ids .website_ids .store_ids .has_options .product_options .type_has_options .required_options (bool) .cart_qty .parent_product_id .stick_within_parent .options_validation_fail events:

  • catalog_model_product_duplicate

  • catalog_product_is_salable_before

  • catalog_product_is_salable_after

  • catalog_product_delete_after_done

  • catalog_product_validate_before

  • catalog_product_validate_after

  • is super

product_type:

-factory - product_type -price factory

mytype:

.setConfig(from_file) .setProduct() - not always! only if not singleton .beforeSave() .save() -- after save .getSetAttributes() .isSalable() .isComposite() via _isComposite .hasOptions() .hasRequiredOptions() .canConfigure() via _canConfigure .canUseQtyDecimals() via config .getSku() .getWeight() .isVirtual() .processBuyRequest() -- convert buyRequest back to options to configure!

  1. when configure .checkProductConfiguration() -- return errors when configuring added product. not to extend
  2. when configure .assignProductToOption()
  3. quote_item_collection._afterLoad > _assignProducts > process each item option[] ._prepareProduct() -- buy request
  4. prepareForCartAdvanced -- return cart candidates .checkProductBuyState() -- check product has all required options. read product.getCustomOption, @throws
  5. quote_item.checkData() -- checks if product in cart is healthy .getProductsToPurchaseByReqGroups
  6. cataloginventory/stock_item.getStockQty .getOrderOptions() .getSearchableData() - delegate to product_option by default .getRelationInfo() - [table, parent_field_name, child_field_name, where] - used in:
  7. fulltext search to get attributes of product+children
  8. product flat indexer - insert children into flat .getChildrenIds()
  9. stock per website, when changing parent inventory to IN_STOCK, at least one child must be IN_STOCK, or will not set
  10. when apply catalog rule, mark active quotes with children => trigger_recollect=1 .getParentIdsByChild()
  11. indexer
  12. inventory stock change

configure product

checkout_cart_configure helper catalog/product_view.prepareAndRender -- pass buyRequests helper catalog/product.prepareProductOptions: - product.processBuyRequest - type.processBuyRequest -- add specific options to buy request - type.checkProductConfiguration - type.prepareForCart -> prepareForCartAdvanced - _prepareProduct - product.setPreconfiguredValues (some options, qty)

product.getPreconfiguredValues

  • from type.processBuyRequest -- convert buyRequest back to options!

mytype_price:

  • get price
  • get base price -- min: group price, tier price, special price
  • get final price = base price + event 'catalog_product_get_final_price' + options prices -- regular price on product view + price of added product accounting for qty
    • unleass there's shortcut -- product.setCalculatedProductPrice() by
  • calculatePrice -- unused?
  • get child final price -- only for bundle
    1. total: address subtotal
  • _get group price -- price by customer group
  • _get tier price -- price by qty
  • calculate price
    1. catalogindex/data_abstract.getFinalPrice() - special price (from-to) vs catalogrule price
  • _calculate special price

product collection:

  • addStoreFilter
  • addWebsiteFilter
  • addCategoryFilter
  • setVisibility
  • addPriceDataFieldFilter (%s < %s, [final_price, price])

Bundle

  • get price fixed -> as defined -- product.getPriceType = Mage_Catalog_Model_Product_Type_Abstract::CALCULATE_PARENT dynamic - 0 -- product.getPriceType = Mage_Catalog_Model_Product_Type_Abstract::CALCULATE_CHILD
  • get final price base + options + selections

Configurable

  • get final price - same + selection extra

product_option: (e.g. extra ingridients)

is_delete is_require .saveOptions()

product_option_type_{group_type}: (default, select, text, file, date)

  • set option, set product
  • validateUserValue(buy_options) -- check required, max chars, dropdown single selection, file max width/height, parse date
  • prepareForCart -- format selection for storing: concatenated ids for dropdown, format date

product_option_value: (e.g. pepper, mustard, ketchup)

buyRequest

  • qty
  • product -- child for configurable
  • related_products
  • super_product_config[product_id] -- parent
  • super_product_config[product_type]
  • bundle: []super_group_{id} -- qty, show only if item salable
  • options
  • reset_count, id

custom options - go to quote_item.options

  • info_buyRequest - serialized
  • product_type (of parent + parent product id)
  • option_ids -- keys of product_type._prepareOptions
  • option_{id} = {value} -- values of product_type._prepareOptions
  • product_qty_{product_id}

configurable:

parent.quote_item_options:

  • attributes = serialize attributes
  • product_qty_{simple_id} = 1
  • simple_product = object
  • option_ids, {ids,ids,..}
  • option_{id}, {value}, option_{id}, {value}, ...

simple.quote_item_options:

  • option_{id}, {value}, option_{id}, {value}, ... -- copy product options from parent to simple
  • parent_product_id = {parent_id}

bundle

parent.quote_item_options:

  • bundle_option_id
  • bundle_selection_ids

each of selection[].quote_item_options: parent_product_id

  • bundle_option_ids
  • bundle_selection_attributes
  • bundle_identity

Product in catalog

  • can configure, is saleable --> add to cart btn/view details/out of stock
  • has required options --> build link to product

View product

  • hasOptions - options price jsonConfig
  • getSetAttributes - show additional data (visible on front)
  • is salable - display availability, show buy btn
  • is salable and has options - display product options
  • is composite - cataloginventory.getStockQty - as is or sum
  • getProductsToPurchaseByReqGroups - cataloginventory.getStockQty - only for composite

Add to cart

quote.addProduct() quote.addProductAdvanced() -- full process mode; create quote_items, add qty type.prepareForCartAdvanced() = type._prepareProduct() type._prepareOptions () -- return options [option_id] = option event sales_quote_product_add_after event checkout_cart_product_add_after collect totals, collect rates cart.save event checkout_cart_add_product_complete

checkout session

  • last added product id
  • quote id
  • cart was updated
  • no cart redirect

View order in admin

Edit order in admin

Downloadable:

-links_purchased_separately -> has options, has required options, link selection required when prepare product -price model.get final price: base price + . + options

db:

  • downloadable/link
  • downloadable/sample
  • downloadable/file

options:

  • is_downloadable = true
  • real_product_type = downloadable

Grouped:

  • price model.get final price
  • composite
  • associated products
  • product form:
    • []super_group_{id} -- qty, show only if item salable
  • custom options:
    • attributes

Configurable

  • configurable attributes (table product_super_attribute_id)

Bundle

  • custom options:
    • bundle_selection_ids
    • selection_qty_{selection_id}

price_index (catalog_product_index_price):

  • entity_id, website_id, customer_group_id, tax_class_id --- base prices w/o options
  • price - as entered in admin
  • final_price - as a group member, my base price is this --- display in catalog grid --- if I buy just a bit, I get my group price + options
  • group_price - (price,final_price) + min options --- optional
  • min_price - (price,final_price) + min options
  • max_price - (price,final_price) + max options --- if I'll buy a lot, I'll get tier_price
  • tier_price - best I can get + min options --- optional

Catalog/config

getProductAttributes = getAttributesUsedInProductListing = resource.getAttributesUsedInListing:

  • crosssell

Indexing

resource catalog/product_indexer_price.getTypeIndexers catalog/product_type.getTypesByPriority indexer:

  • set type id

  • set is compisite

    cataloginventory_stock cataloginventory/indexer_stock ┠catalog_product_attribute catalog/product_indexer_eav ┗catalog_product_price catalog/product_indexer_price catalog_url catalog/indexer_url catalog_category_product catalog/category_indexer_product catalogsearch_fulltext catalogsearch/indexer_fulltext

1 column: mode manual and missed auto update event 2 column: has lost index events

index/indexer:

  • logEvent - create index_event, process[].register,matchEvent -> indexer.register,matchEvent,_registerEvent e.g. before delete store, stock item after save, before delete eav attribute
  • processEntityAction - logEvent + 2 events + indexEvent (process[].safeProcessEvent,matchEvent,processEvent,matchEvent -> indexer.processEvent,matchEvent,_processEvent)
  • indexEvents - process unprocessed. 2 events + process[].indexEvents, indexer.matchEntityAndType, process._processEventsCollection,processEvent -> indexer.processEvent,matchEvent,_processEvent i.e. process existing unprocessed row events collection

processEntityAction - log + register + process, full suite logEvent - only insert, no register/process indexEvents - only process new, created by log, e.g. recursively created events after indexes worked once. call at the end

indexer_abstract:

  • _registerEvent - write data to event object
  • _processEvent - do actual work, delegate to resource

global: end_index_events_* - index/indexer.indexEvents end_process_event_* adminhtml: after_reindex_process_*

(index/indexer)->logEvent(entity, entityType = 'catalog_product', eventType = 'delete') save row in index_event registerEvent index/index -> []indexer(price,url,tags,...).register abstract_indexer[].register matchEvent, _registerEvent... PriceProcessIndexer.register matchEvent _registerEvent -- specific to price process indexer write some generic metadata to event object productTypePriceIndexer[].registerEvent()

(index/indexer)->processEntityAction(entity, entityType = 'catalog_product', eventType = 'save') every matched indexers - register: addProcessId(), addNewData(). each process_id => insert index_process_event, status every matched indexers - safeProcessEvent: lock file var/index_process_{$id}.lock processEvent

process: index/process (index_process) - index management page - indexer_code, status, mode, started at, ended at process.getIndexer(indexer_code)

<global>
  <index>
    <indexer>
      <myindexercode>
        <model></model> <!-- Mage_Index_Model_Indexer_Abstract -->

indexer:

  • name, description, is visible
  • _matchedEntities [entity, actions[]]
  • reindexAll -> resource.reindexAll
  • register
  • registerEvent

priceIndexer._registerEvent - change process status or write some ids, let product type price indexers take part (mostly useless) priceIndexer._processEvent - reindexProductIds() or reindexAll() or callEventHandler() # TODO - research product_type_price_indexers role here

Price Indexer:

catalog/product_indexer_price mymodule/proPimcore duct_indexer_price

model catalog/product_indexer_price -> resource catalog/product_indexer_price _registerEvent: write some data to event depending on changes:

  • product_type_id={id}, reindex_price=1 -- if (options|website|relations|prices) changed
  • reindex_price_parent_ids -- if deleted a product and parents found in catalog/product_relation
  • reindex_price_product_ids={mass_ids}
  • id
  • catalog_product_price_reindex_all=true for each product type, product_type_<price_indexer>.registerEvent --- each type indexer add own metadata. why call all? _processEvent: resource.reindexProductIds
    1. save product? catalog_product_save_commit_after -> catalogrule observer applyAllRulesToProduct -> processEntityAction(product, reindex_price) resource.reindexAll
    2. import products: convert_adapter_product.finish -> processEntityAction(import, save) dynamic resource.{entity}_{type}() or resource.{type} catalogProductSave() -- if (options|website|relations|prices) changed ! productTypeIndexer.reindexEntity(product_id) -- parent and all children collect product_price_indexer_final_(idx|tmp) in multiple steps move to product_price_indexer_(idx|tmp) copy index data by product_id to main table product_index_price catalogProductDelete() -- if deleted a product and parents found in catalog/product_relation ! productTypeIndexer.reindexEntity(parents_ids) catalogProductMassAction() if affected > 30% of all products, reindex all -> type_indexer[].reindexAll else type_indexer[].reindexEntity(ids)

Type Index

  • registerEvent -- for logEvent and processEntityAction
  • reindexEntity(ids) -- one or multiple
  • reindexAll

Flat Indexer

model catalog/product_indexer_flat -> singleton catalog/product_flat_indexer -> resource catalog/product_flat_indexer on product save, mass action on eav attribute save, delete on store save, delete on store group save on import products

eav attributes shown in flat category:

  • system attributes:
    • hardcoded - 'status', 'required_options', 'tax_class_id', 'weight'
    • from **(global/catalog/product/flat/attribute_nodes) => frontend/product/collection/attributes: -- why reference node path in other node???? <url_key/> <special_price/> <special_from_date/> <special_to_date/> <short_description/> <small_image/> <image_label/> <thumbnail_label/> <small_image_label/> <tax_class_id/> <news_from_date/> <news_to_date/> <created_at/> <updated_at/>
  • used_for_sort_by
  • used_in_product_listing
  • is_used_for_promo_rules
  • is_filterable by default DISABLED in global/catalog/product/flat/add_filterable_attributes

mage_catalog_model_resource_product_flat_indexer:

  • updateStaticAttributes
    • getAllAttributes -- backend_type == 'static'
  • updateEavAttributes:
    • getAllAttributes -- backend_type <> 'static'
    • updateAttribute

Categories

Helper catalog/category_flat

  • isAvailable - enabled and indexer not running and no lock file
  • isBuilt - table not empty

Category model

  • getTreeModel, getTreeModelInstance

  • move - 2 events before, 2 events after, event category_move, indexer.processEntityAction, clean cache by tag catalog_category_tree_move_before, catalog_category_tree_move_after catalog_category_move_before, catalog_category_move_after

  • getProductCollection

  • getStoreIds

  • getPathIds, getUrlPath, getLevel

  • getParentId, getParentIds

  • getParentCategory

  • getParentDesignCategory - Category

  • isInRootCategoryList

  • getAllChildren - = by default joined ids ("2,3,4...") + parent id, recursive, only active

  • getChildren - joined ids ("2,3,4..."), only immediate, only active

  • hasChildren - recursive, only active

  • getAnchorsAbove - array of ids - parents that are anchor

  • getProductCount - only directly assigned, all

  • getCategories - tree|collection|Category[]. recursive, only active, only include_in_menu, rewrites joined

  • getParentCategories - Category[], only active

  • getChildrenCategories - collection|Category[], only active, only immediate, include_in_menu*

  • getChildrenCategoriesWithInactive - collection|Category[], all immediate, include_in_menu*

count($collection) = load() + count(items) $collection->getSize() = smart COUNT() SQL count($array) count(tree) = count(nodes) -- immediate or recursive?

Resource shared methods:

  • checkId
  • getAllChildren - array of ids+my id
  • getChildren* - array of ids, by default recursive, only active
  • getChildrenAmount - by default only active (or only inactive), recursive
  • getCategories* - tree|collection|array
  • getParentCategories*
  • getChildrenCategories
  • getParentDesignCategory
  • getChildrenCategoriesWithInactive*

Only non-flat methods:

  • changeParent
  • findWhereAttributeIs
  • getChildren* - only active, by default recusive
  • getChildrenCategoriesWithInactive* - returns Category collection
  • getCategories* - by default node_tree (or collection) + parent id, recursive, by default (active+include_in_menu), by default with rewrites. node tree|collection
    • Varien_Data_Tree > Varien_Data_Tree_Dbp > resource catalog/category_tree
    • Varien_Data_Tree_Node_Collection - actual node storage
    • Varien_Data_Tree_Node - holds tree, parent and children collection
    • - attributes to load
  • getChildrenCategories* - collection, only immediate, only active, with rewrites

Only flat methods:

  • verifyIds
  • getAnchorsAbove
  • getChildren* - by default active, by default recusive
  • getChildrenCategoriesWithInactive* - returns Category[]
  • getCategories* - resursive, by default active, only include_in_menu, rewrites always joined. array|collection
    • categories can be excluded by event 'catalog_category_tree_init_inactive_category_ids'
    • attributes can be added to select in event 'catalog_category_flat_loadnodes_before'
  • getChildrenCategories* - Category[], only immediate, only active, only include_in_menu, with rewrites

CATALOG RULES

invalidate cache -- by default block_html, can add more in <related_cache_types>, e.g. amasty collections data

model.applyAll - full rework, recreate matching products, reindex rule prices, reindex all prices, clear cache resource.applyAllRules - only reindex rule prices

Events:

  • product_type_indexer.prepareFinalPrice "prepare_catalog_product_price_index_table"
  • alter configurable price "catalog_product_type_configurable_price"
  • before save product - mark which rules is already matched. why?
  • after save product - model.applyAllRulesToProduct -- add/delete to matched, reindex
    • resource.applyToProduct
    • resource.applyAllRules
    • invalidateCache
  • product import before finish - add/delete to matched, only imported
  • product import after - resource.applyAllRules - reindex all catalogrule prices
  • after delete catalog attribute - remove from conditions, model.applyAll if found (full rework)
  • after saving attribute, uncheckign 'use in promo rules' - remove from conditions, model.applyAll if found (full rework)

tables to note:

  • catalogrule_product -- matched products to rules, used for indexing prices
  • catalogrule_product_price -- joined when indexing final price ? catalogrule_group_website

cronjob 1 am, same as "Save and apply", same as "Apply rules" -- model.applyAll - full rework:

  • for every rule, call resource.updateRuleProductData(rule) -- update matched products
    • clean all matched products by rule
    • for each store, insert matched products by conditions
  • resource.applyAllRules() -- reindex catalogrule prices for date period
  • invalidateCache
  • price indexer.reindexAll -- our event listener will join catalogrule prices table

model rule/abstract:

ancestor for:

  • catalogrule
  • salesrule
  • enterprise targetrule
  • enterprise reminder
  • enterprise customersegment methods:
  • {get,set}Conditions
  • {get,set}Actions
  • getConditionsInstance
  • getActionsInstance
  • loadPost
  • validate
  • vaildateData
  • getProductFlatSelect - product flat select + catalog_product + conditions SQL

LAYERED NAVIGATION

catalog_product_attribute catalog/product_indexer_eav:

product save/delete/massaction attribute save import products

only indexable attributes:

  • filterable (layer), filterable in search (layer), visible in advanced search
  • only select (int,select)/multiselect (varchar,multiselect)/decimal types, but not price

catalog_product_index_eav --- product-attribute-value_id. when filtering by manufacturer, filter products where attribute = manufactuerer and value = requested catalog_product_index_eav_decimal

eav_attribute_option (dropdown ordered ids by attribute) eav_attribute_option_value (dropdown text values) catalog_product_eav_indexer_(idx|tmp)

layer attribute.applyFilterToCollection -- join catalog_product_index_eav same for decimal

TAX

tax class (product/customer) tax_caltulation_rule - nothing interesting tax_calculation_rate - tax_calculation -- rate, rule, customer tax, product tax

so, I know customer class (by customer group) and product class, I only to choose rule and rate. rules are mostly dumb, so rate decides! rate - location and rate!

tax can be based on: billing/shipping/origin/default country,region,postcode

  • Billing
  • Shipping
  • Origin (Shipping settings - Origin - Country,Region,Postcode)
  • Default (Tax - Default Tax Destination Calculation - Country,Region,Postcode) -- cannot be explicitly chose, fallback when address not available and customer default emtpy

getRateInfo

  • select matching filtering by customer class, product class and location
  • {value, process}
    • rate value = complex percent of all matched (unless 'calculate_subtotal' flag) --> product.tax_percent
    • calculation process = array of logged calculation steps --> product.applied_rates

CATALOG INVENTORY

cataloginvetonry_stock - dummy 1 cataloginventory_stock_item - workhorse, contents of inventory tab on product edit cataloginventory_stock_status - index table, product-website-qty-status. resource indexer.reindexEntity() cataloginventory_stock_status_indexer_idx - index table, same. resource indexer.reindexAll(). same as above. why?

influence catalog listing, product view page? decrement?

product block.displayProductStockStatus() - event catalog_block_product_status_display - from global system config

if not manage stock, always is in stock

is_in_stock - what admin entered stock_status = is_in_stock = is salable

'catalog_product_load_after' -> addInventoryData

stockItem.assignProduct

  • assign product.stock_item, product.is_in_stock
  • stock_status.assignProduct
    • resource stock_status.getProductStatus
    • product.is_salable = read cataloginventory_stock_status.stock_status. why not just stock_item.is_in_stock?

'catalog_product_collection_load_after' -> addStockStatusToCollection

stock_status.addStockStatusToProducts ! product.stock_item may be mocked = Object.is_in_stock = is_salable = stock_status

when reindexing prices - filter only in stock if hide out of stock

when loading quote_item collection -> assign products -> assign stock_item, is_in_stock, is_salable

quote_item.setQty

  • check is_in_stock (and parent if exists), add validation error to quote and item
  • check all options qty (e.g. configurable, bundle), stock_item.checkQtyIncrements, validate qty increments, add error to quote and item
  • check min sale qty by customer group
  • check max qty
  • check backorders

sales_model_service_quote_submit_before (by service_quote.submitOrder)

  • subtractQuoteInventory (once)
    • check qty or throw error
    • substract qty (unless disabled in config)

sales_model_service_quote_submit_failure

  • revert quote inventory

sales_model_service_quote_submit_success (by service_quote.submitOrder)

  • reindexQuoteInventory
    • affected stock
    • affected price

checkout_submit_all_after (after order created, by admin/checkout/api)

(once)

  • subtractQuoteInventory (once)
    • check qty or throw error
    • substract qty (unless disabled in config)
  • reindexQuoteInventory
    • affected stock
    • affected price

sales_order_item_cancel

  • add qty to stock
  • set is_in_stock (if config enabled)

sales_order_creditmemo_save_after

  • if config auto_return_refund, add qty back to items

after saving product - saveInventoryData

duplicate product - copyInventoryData

update system config inventory ?

  • updateSetOutOfStock
  • updateSetInStock
  • updateLowStockDate

CART, QUOTE, ADD TO CART

/checkout/cart/add cart model.add to cart (request) quote.prepare add to cart advanced protuct type.prepare add to cart advanced - add custom options

checkout/cart/*

  • /add
    • params: qty, product, related_product, super_product_config
    • cart.add product
      • check min sale qty
      • quote.addProduct = quote.addProductAdvanced
      • event checkout_cart_product_add_after
    • cart.add product by ids (related)
      • quote.addProduct
    • event checkout_cart_add_product_complete
  • /addgroup -- reorder from sidebar "Last Ordered Items" -- from last order, up to 5 random (hardcoded)
  • /delete
  • /configure
  • /updateItemOptions
  • /updatePost
  • /estimatePost
  • /estimateUpdatePost
  • /couponPost
  • /ajaxDelete
  • /ajaxUpdate

model checkout/cart

  • init - remove paymens, addresses, (and rates if empty)
  • get items -> quote items collection
  • get quote product ids -> quote items -> []product_id (including children)
  • get product ids - same?
  • add order item - add product from order item (reorder)
  • add product
    • check min sale qty
    • quote.addProduct = quote.addProductAdvanced
  • add product by ids -- add related in addition to main
    • quote.addProduct
  • suggest items qty - when calling 'update_qty' -- quote_item.suggestQty. fixes min/max/increments
  • update items + events before/after
  • update item -> quote.updateItem
  • remove item -> quote.removeItem
  • save,saveQuote + events before/after -- save addresses, collect shipping rates and totals
  • truncate -> quote.removeAllItems()
  • get summary qty -- depending on config checkout/cart_link/use_qty
    • "Display item quantities" -> get items qty
    • "Display number of items in cart" -> get items count
  • get items count -> quote.getItemsCount
  • get items qty -> quote.getItemsQty

model quote

  • checkout method -- guest/register
  • get items count, get items qty
  • update item
  • remove item
  • add product = add product advanced
    • product type instance! .prepareForCartAdvanced -- can return error as string. return products with qty, cart_qty (optional) and custom options (buy request, selected options, parent link)
      • _prepareProduct -- return all products to be added, e.g. many for bundle, grouped, parent and child for configurable. Hook here for extra logic
        • _prepareOptions -- can return error as string, [option_id] = "option specific value", e.g. date timestamp, text comment etc, ids for dropdown
        • check and save parent when super_product given (e.g. configurable)
        • wrap product custom options
        • store options in product.customOptions
      • process file queue -- added by _prepareOptions > option_type_file.prepareForCart > addFileQueue
    • for each candidate:
      • quote._addCatalogProduct -- find or create quote_item for product, move product.custom_options > quote_item.custom_options. sales_quote_add_item event
      • quote_item.addQty (product.cart_qty)
      • event sales_quote_product_add_after

TOTALS

quote

-collectTotals

  • []address.collect totals
  • getTotals -- only assigned when total[].fetch via address.addTotal
    • billing address.getTotals if virtual (retrievers order)
    • shipping address.getTotals
    • add/merge all addresses.getTotals
    • sort again

! quote.trigger_recollect - schedule collectTotals and save on next load

quote.collectTotals

  • events sales_quote_collect_totals_before, sales_quote_collect_totals_after
  • clear {base,}{subtotal,subtotal_with_discount,grand_total}
  • process addresses[]
    • clear {base,}{subtotal,grand_total}
    • address.collectTotals
    • quote.{base,}{subtotal,subtotal_with_discount,grand_total} += address.value
  • validate grand_total not ∞
  • calculate items_count, items_qty
  • validate coupon_code -- must exist in address as well

quote address

  • collectTotals

  • set{base,}TotalAmount(code) -- 1) sets values on address 2) summed by grand_total

  • get{base,}TotalAmount(code) -- for reading by other totals

  • addTotalAmount

  • getAll{base,}TotalAmounts -- used by grand total to assign and display grand_total

  • getTotals -- beware! non-caching

  • addTotal(data) -- array wrapped into sales/quote_address_total and assigned address

address.collectTotals

  • events sales_quote_address_collect_totals_before, sales_quote_address_collect_totals_after
  • total collector[].collect(address)

total collector

collect, fetch cached in config cache

collectors: one,two... one,two... block/here <sort_order>0</sort_order> <nominal_totals> </nominal_totals> <order_invoice> </order_invoice>

retrievers (optional, without sort goes last): <totals_sort> 10

totals | retrievers

nominal (tricky) | + subtotal subtotal | + discount freeshipping | + shipping giftwrapping | + tax <taxes*> (if config tax/cart_display/grandtotal) tax_subtotal | weee msrp | + reward weee | + giftcardaccount shipping | + customerbalance tax shipping | + grand_total

discount | + nominal tax | msrp tax_giftwrapping | freeshipping grand total | tax_subtotal reward | tax_shipping giftcardaccount | + giftwrapping customerbalance | + tax_giftwrapping

nominal totals

collect --- strings all nominal totals.collect each nominal_total.collect fetch: if address has nominal items, just add 1 total with them

recurring_initial_fee recurring_trial_payment nominal_shipping nominal_discount nominal_tax_subtotal nominal_tax

properties to extend:

  • _canSetAddressAmount -- can disable _setAmount (=address.setTotalAmount). disabled in nominal-shipping
  • _canAddAmountToAddress -- can disable _addAmount. disabled in all nominal-*.
  • _itemRowTotalKey interface:
  • processConfigArray(config) -- can change sort order
  • collect: ...
    • address.setTotalAmount(code, amount) -- 1) assigns value - address.{code_amount} = amount 2) address._totalAmounts.{code} = amount
    • address.addTotalAmount(code, amount)
    • _setAmount = address.setTotalAmount if allowed
    • _addAmount = address.addTotalAmount if allowed ...
  • fetch:
    • custom logic...
    • address.addTotal(code, as, title, value, area) -- area = -1 -- include everywhere? -- as lets user total renderer of other guys

render totals

by checkout/cart_sidebar, checkout/cart_totals

  • renderTotals
    • []renderTotal(total)
    • get total renderer:
      • block {code}_total_renderer if present
      • 'renderer' from total config
      • checkout/total_default
      • assign totals -- all totals
    • assign (total, colspan, renderingArea) -- specific total

CART PRICE RULES

salesrule - workhorse, grid + basic params + actions + conditions salesrule_label - translations for stores salesrule_customer_group -- assign groups, has-many salesrule_website -- assign websites, has-many salesrule_coupon -- type (0-auto, 1-specific-auto generated and manual), expiration (not applicable when coupon type=no coupon) salesrule.coupon-type = specific, use_auto_generation, salesrule_coupon.type = 1 salesrule.coupon-type = specific, no generation , salesrule_coupon.type = 0 salesrule.coupon-type = auto, -------------------, salesrule_coupon.type = 0 coupon_aggregated coupon_aggregated_order -- by orders.created at coupon_aggregated_updated -- by orders.updated at

! sales totals discount -- not used salesRule totals discount - salesrule/quote_discount

salesrule_coupon_usage -- coupon usage by customer salesrule_customer -- rule usage per customer salesrule_product_attribute -- attributes used in actions/conditions -- selected by quote_item collection after load -> assign products -> add to select (used in promo) (sales_quote_config_get_product_attributes in 'sales/quote_config'->getProductAttributes

! {quote_item,address,quote}.applied rule ids

simple action:

  • by_percent -- per item
  • by_fixed -- per item
  • cart_fixed -- quote.subtotal
  • buy_x_get_y

Discount Totals

-- start processing from parent items -- address: discount description, shipping discount amount, discount amount -- quote_item: discount amount, discount percent

collect:

  • set discount_amount 0
  • calculator.init -- load all sales rules matching website-customergroup-quotecoupon
  • calculator.initTotals -- for future items calculation by rules of action cart_fixed
    • process rules with action = cart_fixed
      • if can process rule -- conditions and coupon ok
        • check each quote_item[] -- rule.actions.validate
        • store total matched price and count in _rulesItemTotals
  • !extension chance - quote item.no_discount
  • for parent and each child item[]
    • event sales_quote_address_discount_item
    • ! calculator.process
    • aggregate item discount -- negative address.discount amount -= item.discount amount
  • process weee discount (if enabled in weee config)
  • ! calculator.process shipping amount
  • negative address.discount amount -= address.shipping discount amount

can process rule - \Magento\SalesRule\Model\Utility::canProcessRule

  • if coupon applicable, load coupon, check coupon.usage limit, coupon.usage per customer usage per coupon/customer (coupon_usage)
  • check rule.uses per customer (rule_customer)
  • rule.conditions.validate(address)

process quote item

for each rule (until stop processing met):

  • if can process rule -- coupon and global conditions are ok
  • if rule.actions.validate(quote item)
    • work ... depending on simple action ... item.discount percent, {base,}discount_amount
    • event salesrule_validator_process ! {quote_item,address,quote}.applied rule ids

fetch

add total {discount amount, discount description}

Freeshipping total collect

  • calculator.init
  • calculator.processFreeShipping (quote_item)
    • for each rule:
      • if can process rule -- coupon, conditions ok
      • if rule.actions.validate(quote_item)
      • if simple free shipping
        • per item -- quote_item.free shipping = "Maximum Qty Discount is Applied To" ?: true
        • whole address -- address.free shipping = true
      • stop rule processing if set

events

after place order:

  • rule.times_used++
  • rule_customer.times_used++
  • coupon_usage.times_used++

sales_quote_config_get_product_attributes:

  • add used attributes from table

convert quote to order:

  • set order.coupon_rule_name

after delete attribute in admin:

  • if attribute was used in promo, remove it from used rules

after unchecking 'used in promo':

  • remove from rules

cron - at 00 am every night

aggregate rules for previous day:

  • createdat -- {day,store,order status,coupon_code, .... other boring stats .... }
  • updatedat

PAYMENT METHODS

global/payment/cc/types/{type}/code,name,order global/payment/groups

Recurring profiles (magento) - recurring payment. e.g. magazine subscription, phpstorm subscription :)

  • only Paypal express
  • show as nominal items
  • order not created ---- how to manage?????????????????????
  • purchased separately from other items
  • not added to totals
  • nominal grand total
  • shipping only fixed, rate and free (methods with carrier._isFixed = true)
  • method instance must implement Mage_Payment_Model_Recurring_Profile_MethodInterface

billing agreements

  • remember my payment, e.g. save credit cart on AliExpress

  • shown as separate method

  • manage on custom account page

  • charged directly via API without interaction

    <title></title/>

onepage.saveShipping: addressForm = customer/form customer/form:

  • form_code
  • entity_type
  • is_ajax_request
  • prepareRequest(POST) - clone request, clear and reassign only POST
  • getAttributes: collection '{module}/form_attribute_collection'
    • all attributes from customer_form_attribute
    • by entity_type, form_code, but resource is customer/form_attribute
  • extractData(data):

Flow

guest checkout/registration

quote.checkout_method = Mage_Checkout_Model_Type_Onepage::METHOD_REGISTER quote.password_hash

checkout/onepage/saveMethod - method = register (or guest)

-- show billing address + select shipping: same/different onepage.saveCheckoutMethod:

  • quote.setCheckoutMethod -- register/guest

checkout/onepage/saveBilling

  • selected existing customer address:
    • billing[address_id]
    • billing[...] other fields sent anyway! copied from default customer billing address
  • selected new billing address:
    • billing[address_id] still selected. wtf? not used?
    • billing[...]

... create account - quote: guest:

  • customer_email
  • customer_is_guest
  • customer_group_id register:
  • quote.getCustomer - new customer
  • quote.setCustomerId(true) - will be picked up in service_quote.submitOrder, if(customer) transaction.addObject(customer)
  • customer.addAddress(billing.exportCustomerAddress)
  • customer.addAddress(shipping.exportCustomerAddress)
  • shipping.customerAddress = ... sames as ? ...
  • password, password hash
  • quote.setCustomer after submit quote -send email customer:
  • {billing,shippingn}: new address or save in address book? customer.addAddress(quote address.exportCustomerAddress)
  • .... is default {billing,shipping}
  • quote.setCustomer

checkout/onepage/saveShippingMethod -- return payment methods

get payment html -> handle checkout_onepage_paymentmethod

  • event checkout_controller_onepage_save_shipping_method -- save specific params from shipping step, e.g. giftmessage, giftwrapping
  • return block checkout/onepage_payment_methods instanceof payment/form_container (checkout/onepage/payment/methods.phtml)
    • in _prepareLayout create form blocks for each method and set a child payment.method.{code} helper payment.getMethodFormBlock(method) create block _formBlockType block payment/form.setMethod

    • in template foreach block payment/form_container.getMethods

      1. helper payment.getStoreMethods -- return all, filter isAvailable
        • config.active
        • event payment_method_is_active
      2. method.isApplicableToQuote -- check country, currency, min-max, zero total
    • print method title

      • form_block.method_title if present
      • else config.title
    • print method form - render form_block

      • can change template from layout: ....... ... fill in form fields unser name payment[], submit to checkout/onepage/savePayment

checkout/onepage/savePayment -- redirect or go to order review

  • address.set payment method -- not saved in DB?
  • quote.payment.importData -- quote.payment == just container class, holds specific method_instance
    • event sales_quote_payment_import_data_before -- modify POST fields from method -- import specific values
    • quote.collectTotals
    • !method.assignData -- write needed data from POST to quote.payment fields
    • !method.validate -- by default check canUseForCountry - billing country. @throws
    • quote payment._beforeSave: !method.prepareSave
  • quote.save -- saves payment info as well ! redirect opportunity ! quote.payment.getCheckoutRedirectUrl -> method.getCheckoutRedirectUrl

checkout/onepage/saveOrder

payment data in POST again quote_payment.importData again onepage.saveOrder:

  • prepare quote depending on mode
    • guest: quote.customer_email (from billing.email), customer_is_guest=1, customer_group_id=0, customer_id=0
    • register: ... quote.password_hash (not erased?!), customer_id
    • customer ...
  • !service_quote.submitAll
  • involve new customer if registration (send confirmation/success email, auto login)
  • {+} checkout_session:
    • lastQuoteId
    • lastSuccessQuoteId
  • {-} checkout_session.clearHelperData:
    • last billing agreement id
    • redirect_url
    • last order id
    • last real order id
    • last recurring profile ids
    • additional messages
  • event checkout_type_onepage_save_order_after
  • queue new order email (unless ...)
  • {+} checkout_session:
    • last order id
    • ! redirect_url -- method.getOrderPlaceRedirectUrl
    • last real order id (increment)
    • last billing agreement id*
    • last recurring profile ids*
  • event checkout_submit_all_after ! redirect to checkout_session.redirect_url (optional)

submit_all

submit nominal items:

  • validate addresses, each recurring item[].submit and delete submit order:
  • quote._validate -- addresses, shipping and payment method
    • {shipping, billing} address._validate
      • basic check - firstname, lastname, street, city, telephone postcode (unless optional), country_id, region_id (if required)
      • event customer_address_validation_after
      • should_ignore_validation implicit data flag
    • shipping_method and rates must be selected
    • quote_payment.method must be selected
  • quote.reserve order id
  • order = addressToOrder -- shipping (unless virtual)
    • toOrder
      • new order
      • use reserved increment, assign customer
      • copy fieldset quote -> order
      • event sales_convert_quote_to_order
    • copy fieldset address -> order
    • event sales_convert_quote_address_to_order
  • new order.{billing,shipping} address -> convert fieldset + event sales_convert_quote_address_to_order_address
  • new order.payment -> convert quote_payment fieldset + event sales_convert_quote_payment_to_order_payment
  • assign any order data from _orderData --- by adminhtml when creating order - link to previous order
  • order items -> convert fieldset + event sales_convert_quote_item_to_order_item
  • event checkout_type_onepage_save_order -- not necessarily onepage though?
  • event sales_model_service_quote_submit_before
  • transaction.save
    • customer
    • quote
    • order
    • !order.place
    • order.save -- join store_name, new increment_id if not reserved, total_item_count, protect_code
  • inactivate quote
  • event sales_model_service_quote_submit_success or sales_model_service_quote_submit_failure
  • event sales_model_service_quote_submit_after

order.place

  • event sales_order_place_before
  • order_payment.place
    • event sales_order_payment_place_start
    • payment.{amount_ordered, shipping_amount}
    • method.set store
    • !method.validate -- by default check country
    • default order {STATE_NEW, STATUS from config or default assigned in admin} --- bad, not used in sales reports. should set somewhere
    • !method.getConfigPaymentAction: -- by default config.payment_action, can process
      • no action - leave {STATE_NEW, STATUS default}
      • !method.isInitializeNeeded: -- some custom logic wanted
        • !method.initialize() -- work and update order {state,status}
      • initialize not needed: -- typical flow, admin selected action order/authorize/capture
        • order(full amount): -- rare case
          • !method.order(full amount) -- canOrder + custom logic -- write payment.transactions, additional info, transaction_id, parent_transaction_id
          • implicit SkipOrderProcessing -> stop here
          • implicit IsTransacitonPending -> {STATE_PAYMENT_REVIEW, status default}
          • implicit IsTransacitonPending+IsFraudDetected -> {STATE_PAYMENT_REVIEW, STATUS_FRAUD}
          • implicit PreparedMessage
          • add order transaction (TxnType = order) implicit SkipTransactionCreation, TransactionId, IsTransactionClosed, ParentTransactionId, ShouldCloseParentTransaction
          • order state or default {STATE_PROCESSING, status auto}
        • authorize (full amount):
          • payment.amount_authorized = full_amount
          • !method.authorize(full amount) -- canAuthorize + custom logic. normally create session and reserve/authorize money
          • implicit IsTransacitonPending -> {STATE_PAYMENT_REVIEW, status default}
          • implicit IsFraudDetected -> {STATE_PAYMENT_REVIEW, STATUS_FRAUD}
          • add order transaction (TxnType = authorization) implicit SkipTransactionCreation, TransactionId, IsTransactionClosed, ParentTransactionId, ShouldCloseParentTransaction
          • order state or default {STATE_PROCESSING, status auto}
        • capture:
          • payment.amount_authorized = full_amount
          • payment._invoice
            • new invoice: convert order -> invoice, order items -> invoice items, invoice.collectTotals
            • invoice.register:
              • invoice item[].register -- order item.*_invoiced - qty, taxes, discounts, row
              • can capture and online: invoice.capture()
                • payment.capture()
                  • implicit payment ParentTransactionId,TransactionId
                  • generate transaction
                  • event sales_order_payment_capture
                  • !method.fetchTransactionInfo (if existing transaction id)
                  • implicit invoice IsPaid, IsTransactionPending -- if not yet paid and not pending
                  • !method.capture -- custom logic, check canCapture -- use $payment->getParentTransactionId()
                  • add transaction (TxnType = capture)
                  • implicit IsTransactionPending -> {STATE_PAYMENT_REVIEW, status auto}, invoice is not paid
                  • implicit IsTransactionPending+IsFraudDetected -> {STATE_PAYMENT_REVIEW, STATUS_FRAUD}, invoice is not paid
                  • no implicit pending -> {STATE_PROCESSING, status auto}, invoice is paid
                  • !method.processInvoice --- rare, almost deprecated
                • implicit invoice IsPaid -> invoice.pay()
                  • invoice {STATE_PAID} unless implicit payment ForcedState
                  • payment.pay -- update _paid, _captured
                    • payment.amount_paid,shipping_captured
                    • event sales_order_payment_pay
                  • order.total_paid
                  • event sales_order_invoice_pay
              • can capture and offline: invoice.pay()
              • can't capture and !method.isGateway or offline: invoice.pay()
              • order.*_invoiced -- total, subtotal, taxes, shipping, discounts
              • default invoice state {STATE_OPEN}
              • event sales_order_invoice_register
  • event sales_order_place_after

order vs authorize:

  • order: does not affect values
  • authorize: fraud detection - checks same currency and capture final
  • authorize: payment.amount_authorized = full_amount
  • authorize can be offline

block payment/form for _formBlockType

  • method -- assigned in block payment/form_container._prepareLayout when creating

method_instance implicit data

  • sort_order - when get store methods
  • info_instance -- quote_payment OR order_payment
  • ! store (from quote or order)

method_instance properties

  • _code
  • _canManageRecurringProfiles - only PayPal Express, PayPal BillMeLater (+UK)
  • _formBlockType -- extends block payment/form
  • _infoBlockType -- extends block payment/info
    • _prepareSpecificInformation -- should be varien object
    • event payment_info_block_prepare_specific_information -- if not set explicitly
  • _canUseCheckout
  • _canUseForMultishipping
  • _canUseInternal
  • _canVoid - to cancel online, must exist non-closed auth transaction
  • _canCapture
  • _canCapturePartial - qty editable
  • _canRefund
  • _canRefundInvoicePartial - qty editable
  • _canReviewPayment - when status payment_review, accept/deny buttons will show up

method_instance methods

!assignData -- assign specific fields from POST. write to quote.payment fields !validate -- when saving form data and before placing order !prepareSave -- when quoet payment saved. rare, used only by cc !getCheckoutRedirectUrl -- before order placed. can be in data.

  • if returned, will skip order review and redirect
  • if empty, normal order review step with button "Place order" ! getOrderPlaceRedirectUrl - after order created ! getOrderOptions -- extract/unserialize product.custom_options for external usage by type
  • !don't forget to set 'product_calculations' = parent/child, affects invoicing getConfigData canUseForCountry -- see config.allowspecific canUseForCurrency -- custom canUseCheckout -- _property canUseForMultishipping -- _property canUseInternal -- _property canManageRecurringProfiles -- _property + instanceof recurring isGateway !initialize !order !authorize !capture
  • use $authTransactionId = $payment->getParentTransactionId();
  • is_capture_complete = (int)$payment->getShouldCloseParentTransaction() processInvoice --- rare, almost deprecated fetchTransactionInfo isAvailable:
  • by default from config.active
  • event payment_method_is_active
  • checks recurring (_canManageRecurringProfiles + implement interface)
  1. admin - invoice, refund

Payment Methods

bank transfer

payment.additional information:

  • instructions <- payment_method.getInstructions()

order.cancel

  • can cancel
    • not on hold, not payment review state
    • not canceled, not complete, not closed
    • !not ACTION_FLAG_CANCEL (can set after order load)
  • payment.cancel:
    • online <== method.canVoid and auth transaction found and not closed
    • implicit payment.message
    • if online:
      • method.void
      • insert transactions txntype void
      • order {STATE_PROSESSING, status default}
    • event sales_order_payment_cancel
  • order.registerCancellation:
    • cancel order_items[] -- *_canceled
    • order.*_canceled
    • {STATE_CANCELED, status default}
  • event order_cancel_after

Cancel operations

creditmemo.cancel:

  • creditmemo_item.cancel -- update order.{qty,tax,hidden_tax}_refunded
  • order payment.cancelCreditmemo:
    • order payment.{amount,shipping}_refunded
    • event sales_order_payment_cancel_creditmemo

invoice.cancel -> payment.cancelInvoice (payment fields), fields order.*_invoiced, event sales_order_invoice_cancel, state invoice.void = payment.void (online), invoice.cancel invoice:

  • total_qty
  • base_total_refunded
  • is_used_for_refund

order.cancel -> payment.cancel -- decide online/offline order.registerCancellation -- fields *_canceled, state order:

  • normal totals ....
  • shipping_{canceled,invoiced,refunded,tax_refunded}
  • subtotal_{canceled,invoiced,refunded}
  • tax_{canceled,invoiced,refunded}
  • total_{paid,qty_ordered,canceled,invoiced,online_refunded,offline_refunded}

order_item.cancel -- checks status_id: PENDING - nothing happened SHIPPED INVOICED REFUNDED BACKORDERED CANCELED qty_ordered = qty_canceled PARTIAL MIXED order_item:

  • qty_{ordered,backordererd,canceled,invoiced,shipped,refunded}
  • {tax,hidden_tax}_canceled
  • {tax,hidden_tax,discount}_invoiced
  • {tax,hidden_tax,discount}_refunded

payment.cancel -> _void (online/offline) -- decide online or offline, event sales_order_payment_cancel payment.void -> _void (online) -- event sales_order_payment_void payment.cancelInvoice -- fields payment._paid, payment._captured, event sales_order_payment_cancel_invoice order payment:

  • shipping_{captured,refunded}
  • amount_{ordered,authorized,paid,paid_online,canceled,refunded,refunded_online}

method cancel vs void

method.cancel - void when no invoices, e.g. authorization method.void - actual logic

canVoid - auth transaction found and not closed canCapture - if auth transaction is found but closed, order transaction must be there

void and capture transactions - parent should be auth

order, auth, capture, void, refund

transaction->closeAuthorization: {void,capture}Transaction -> closeAuthorization (parent) authTransaction->close, closeAuthorization transaction->closeCapture: captureTransaction -> close, closeCapture refundTransaction -> closeCapture (parent)

MULTISHIPPING

!method.checkoutRedirectUrl and checkoutOrderPlaceUrl not supported! virtual items assigned to billing address

available:

  • enabled in config shipping > option > allow multi
  • doesn't have items with decimal. why?
  • doesn't have nominal items
  • min amount for each address/total by config sales > min order amount > amount and validate separately
  • not all virtual
  • below max qty per config shipping > option > max
  • quote not virtual

quote.is_multishipping quote_address_item[] -> quote_item

/checkout/multishipping/addresses - each item separately assigned address

  • type_multishipping.init:
    • import customer default {shipping,billing} address and email: copy fieldset customer address -> quote address
    • add all items to default shipping address -> quote_address_items[]
  • split quote_address_item[] by 1 qty -- getQuoteShippingAddressesItems
  • cast qty to int
  • render rows product-qty-saddress

/checkout/multishipping/addressesPost -- same for save changes and continue

  • checkout.setShippingItemsInformation -- saves changes
    • checks max qty
    • event checkout_type_multishipping_set_shipping_items
    • redirects to

/checkout/multishipping/shipping

  • for each quote.getAllShippingAddresses:
    • shipping rates form
    • display address items

/checkout/multishipping/shippingPost

  • event checkout_controller_multishipping_shipping_post
  • save shipping method for each address

/checkout/mutishipping/billing

  • payment methods form for billing address

/checkout/multishipping/overview

  • save selectedpayment - checkout.setPaymentMethod
    • quote payment.importData --- method.assignData, validate
    • collectTotals and rates
  • display selected payment method + !method info block
  • for each address, display selected shipping method and address items

/checkout/multishipping/overviewPost

  • check min amount
  • check required agreements
  • checkout.createOrders:
    • _validate:
      • check method.isAvailable
      • all shipping addresses[].validate --
        • basic check - firstname, lastname, street, city, telephone postcode (unless optional), country_id, region_id (if required)
        • event customer_address_validation_after
        • should_ignore_validation implicit data flag
      • shipping methods and rates must be ok
      • billing address.validate
    • if has virtual, add address for them based on billing
    • for each address:
      • _prepareOrder:
        • order = convert address to order (quote to order)
          • event sales_convert_quote_to_order
          • event sales_convert_quote_address_to_order
        • order billing and shipping address - convert -- event sales_convert_quote_address_to_order_address
        • convert payment -- event sales_convert_quote_payment_to_order_payment
        • for all address items, convert -- event sales_convert_quote_item_to_order_item
      • event checkout_type_multishipping_create_orders_single
    • for each created order:
      • order.place -- events before/after + payment.place -- call method order/authorize/capture
      • order.save
      • queue new email -- mailer.send via setQueue: core/email_queue (entity type order, entity id, event new order)
    • core session.order_ids
    • checkout session.last_quote_id
    • quote set not active
    • event checkout_submit_all_after or checkout_multishipping_refund_all on error
  • session.clear:
    • event checkout_quote_destroy
    • quote_id = null
    • last_successful_quote_id = null

/checkout/multishipping/success

event checkout_multishipping_controller_success_action

SHIPPING METHODS

what I remember:

  • default/carriers/{code}:
    • model
    • title
    • active
    • debug method instanceof Mage_Shipping_Model_Carrier_Abstract: implements Mage_Shipping_Model_Carrier_Interface!
  • isTrackingAvailable
    • creating invoice, can also create shipment. dropdown with carriers
    • creating shipment
    • order view in admin -> link to view/remove
  • !getAllowedMethods: -- consider standard config 'allowed_methods'
    • used in source model adminhtml/system_config_source_shipping_allmethods
    • in turn used in cart price rule as condition "Shipping Method is ..."
  • collectRates(request) request->addRate(new rate) shipping adderss.shipping method shipping rates

shipping address.getGroupedAllShippingRates

-- 1 to check specific available countries -- comma-separated available destination countries -- error if country validation failed. or default -- show error message or just skip carrier

shipping/rate_result - has appended shipping/rate_result_abstract[]:

  • shipping/rate_result_method
    • carrier
    • carrier_title
    • method
    • method title
    • price
    • cost
  • shipping/rate_result_error

global: result = shipping/rate_result -- shipping/shipping.getResult - holds overall result

carrier: result = shipping/rate_result for each sub-method[]:

  • method = shipping/rate_result_method
  • result.append(method)

can return result with methods[] can return just single method

carrier

implicit: setStore -- store_id present setActiveFlag('active') -- stupid checkAvailableShipCountries getConfigData getFinalPriceWithHandlingFee - adds handling_fee, handling_type(fixeD/%), handling_action(per order/package) !proccessAdditionalValidation - when collecting rates, return false to skip !collectRates !isShippingLabelsAvailable !isTrackingAvailable -- when creating shipment:

  • add to carriers in Shipping Information-Add Tracking Number box
  • option to create labels in popup? !isGirthAllowed... -- for packages popup, hardcoded for USPS, only when displayGirthValue !getDeliveryConfirmationTypes -- for packages popup !getContainerTypes !getContentTypes -- documents, other...

!requestToShipment -- do work when creating shipment

quote.collect totals

totals.shipping - sales/quote_address_total_shipping

{weight, free method weight, shipping_amount} = 0

  • sum items:
    • address.item_qty = sum(item[].qty)
    • address.weight = sum(item.qty * item.weight)
    • address.free method weight = 0 for free address, sum of only non-free item weights (item can have max qty for free shipping)
  • collect shipping rates
  • if shipping method selected, copy from according rate:
    • address.shipping_amount = rate.price
    • address.shipping_description = "rate.carrier_title - rate.method_title"

not sorted before collected

shipping address.collectShippingRates

flag collect_shipping_rates -- only once, saved in DB remove all shipping rates - mark deleted, will delete on save !address.country_id is required request shipping rates:

  • new request = shipping/rate_request -- just varien object

    • all_items -- quote_items[]
    • dest_country_id
    • dest_region_id
    • dest_region_code -- 2 letter state code or full region name
    • dest_street
    • dest_city
    • dest_postcode
    • package_value -- address subtotal, includes virtual price
    • package_value_with_discount -- subtotal_with_discount, includes virtual price
    • package_weight -- address.weight
    • package_qty -- address.item_qty - all items
    • package_physical_value -- subtotal - virtual_amount, excludes virtual price
    • free_method_weight -- 0 for address.free_shipping, sum of items weights excluding item.free_shipping max qty usage examples?
    • store_id
    • website_id
    • free_shipping -- whole address
    • base_currency
    • package_currency
    • limit_carrier --collect only specific carriers. examples?
    • base_subtotal_incl_tax -- base_subtotal_incl_tax + base_extra_tax_amount
  • !shipping/shipping.collectRates

    • !request.orig -> set request from config Shipping settings > Origin:
      • country_id
      • region_id
      • city
      • postcode
    • read carriers from default/carriers/
    • for each carrier[], shipping/shipping.collectCarrierRates:
      • carrier = instantiate by and
      • carrier.checkAvailableShipCountries -- by config, not to extend
      • carrier.proccessAdditionalValidation if no country error -- return shipping/rate_result_error on error
      • if carrier config 'shipment_requesttype' ???? examples, result???
        • split packages by config 'max_package_weight'
        • carrier.collectRates for each divided package with reduced weight
        • aggregate prices somehow
      • if not split:
        • carrier.collectRates
      • sort result.rates by price
      • global result.append carrier result rates
  • add result rates to address

  • if shipping_method found in rates, copy address.shipping_amount

if none found, reset {shipping amount,shipping method,shipping description}

split by packages

  • only items with qty_decimals 'Qty Uses Decimals'
  • only items with is_decimal_divided 'Can be Divided into Multiple Boxes for Shipping'
  • splits into packages by max_package_weight
  • not working with bundle
  • prices = collect each package separately and sum each results
  • carrier shipment_requesttype should be 'Use origin weight (few requests)'

Create Shipment

sales/service_order.prepareShipment shipment = convert order > shipment shipment items[] = convert order items[] item[].qty_to_ship = qty_ordered - qty_shipped - qty_refunded - qty_canceled -- submit form shipment.register

  • shipment item[].register -- order item.qty_shipped += qty
  • shipment.total_qty add shipment comment to collection !create shipping labels if supported:
  • carrier.isShippingLabelsAvailable
  • shipment.setPackages from request
  • shipping/shipping.requestToShipment
  • ensure non-empty:
    • admin first and last name
    • store name and phone
    • shipping origin street, city, postcode, country
  • request object:
    • order_shipment -- just shipment --- from admin user
    • shipper_contact_person_name
    • shipper_contact_person_first_name
    • shipper_contact_person_last_name
    • shipper_contact_company_name
    • shipper_contact_phone_number
    • shipper_email
    • shipper_address_street
    • shipper_address_street1
    • shipper_address_street2
    • shipper_address_city
    • shipper_address_state_or_province_code
    • shipper_address_postal_code
    • shipper_address_country_code --- from address
    • recipient_contact_person_name
    • recipient_contact_person_first_name
    • recipient_contact_person_last_name
    • recipient_contact_company_name
    • recipient_contact_phone_number
    • recipient_email
    • recipient_address_street
    • recipient_address_street1
    • recipient_address_street2
    • recipient_address_city
    • recipient_address_state_or_province_code
    • recipient_address_region_code
    • recipient_address_postal_code
    • recipient_address_country_code --- other
    • shipping_method
    • package_weight
    • packages -- from request
      • container, weight, customs_value, dimensions, units, confirmation ... and items
    • base_currency_code
    • store_id
  • !carrier.requestToShipment -- return varien object: errors, []info.tracking_number, []info.label_content
  • !shipment.shipping_label -- parse and generate single PDF from each []info.label_content. can be encoded images or encoded PDF
  • !save each info.tracking_number to order_shipment_track

carrier.getTrackingInfo($track_number)

sales/order/trackinginfo.phtml:

  • carrier_title
  • Tracking
  • Url
  • Status
  • Deliverydate
  • Deliverytime
  • Deliverylocation
  • Signedby
  • TrackSummary

freeshipping:

! if all items free shipping, updates whole request free shipping

flat rate:

  • gather free qty (item.free_shipping ? item.qty) !not respecting max free shipping qty?
  • price = per order - just add config.price, or per item - (package_qty - free_qty)*config.price
  • supports handling fee fixed/% per order/package

tablerate:

  • if not include_virtual_price, updates request.package_value -= virtual items
  • respects max freeshipping qty
  • deducts request.package_value for free and virtual (if not include virtual)
  • resource shipping/carrier_tablerate.getRate select from tablerate where $condition_name(package_weight,package_qty,package_value) <= order value(package_weight,package_qty,package_value)
  • supports handling fee

UPS - united parsel service (world), USPS - united states postal service, FedEx - Federal Express (international), DHL (world)

UPS - United Parcel Service/USPS - United States Post Service/DHL

  • enabled for RMA
  • UPS type
  • container
  • destination type
  • request_type - split/not
  • pickup type
  • max package weight
  • handling fee
  • sub methods
  • !free method --- this is shared for all carriers! _rawRequest, _freeMethod, _setFreeMethodRequest, _getQuotes, _updateFreeMethodQuote
    • if all items are free_shipping > request.free_method_weight = 0, only selected submethod will get price 0
  • Minimum Order Amount for Free Shipping -- if order price is big, free method is searched and set price 0 implements Mage_Shipping_Model_Carrier_Interface:
  • isTrackingAvailable
  • getAllowedMethods

! free shipping method - if order subtotal >= threshold, submethod.price = 0 ! free shipping items support - get rate for "free shipping method" with reduced weight

proccessAdditionalValidation - usa methods additional validation:

  • check max weight -- each item separately
  • dest post code is required (unless - isZipCodeOptional by country)

collectCarrierRates

  • split to packages if Packages Request Type - Use origin weight (few requests)
  • when total weight > max, splits by num_boxes, e.g. total 68 and max 20 -> 4 boxes
  • weight = average between all boxes, e.g. 68 lbs / 20 lbs max = 4 boxes * 17 lbs average
  • when parsing response price:
    • sets price 0 if subtotal > free shipping min amount
    • sets price = num of boxes * cost + fee
  • update free method:
    • fetch only rate with only paid weight: only 2 boxes * average 17.5 lbs
    • cost is the same, but num of boxes is lower, so XPR 900

SALES AND CUSTOMERS

ship separately? taxes when refunding? ------ totals? credit memo totals? partial invoice/ship/refund? cancel order/order item/shipment/invoice/credit memo? taxes when canceling? ----- totals?

customer attributes? customer emails?

create new order

Blocks:

<adminhtml_sales_order_create_index> adminhtml/sales_order_create, adminhtml/sales_order_create_form adminhtml/sales_order_create_customer adminhtml/sales_order_create_store adminhtml/sales_order_create_data _sidebar -- cart, wishlist, reorder, viewed, compared, pcompared?, pviewed? _form_account -- customer form, model customer/customer. attributes from eav/attribute by entity_type customer _shipping_address, _billing_address -- model customer/address, by entity_type address _billing_method + form -- instanceof payment/form_container _newsletter + form -- checkbox _search + grid of products -- add new items, filter only adminhtml/sales/order/create/available_product_types _items + grid -- added products _coupons _comment totals adminhtml_sales_order_create_load_block{header,sidebar,...,totals} -- every block from above </adminhtml_sales_order_create_index>

ajax calls:

  • ..create/loadBlock/block/{name}

  • ..create/configureProductToAdd

  • ..create/configureQuoteItems

  • helper catalog/product.skipSalableCheck -- admin can create any product (ignore inventory)

adminhtml/sales_order_create/start

  • clear session quote (customer_id, store_id, quote_id etc.)
  • redirect ↓ adminhtml/sales_order_create/index
  • init session:
    • event create_order_session_quote_initialized -- only weee copies store_id for self
  • order create form container and form
  • order data json is empty - no customer and store selected
  • 3 inline steps:
    • customer grid, row click callback > loadBlock (header) +customer_id
      • init session - save customer_id to quote session, event create_order_session_quote_initialized
      • processActionData, event adminhtml_sales_order_create_process_data_before, no more post data, nothing to process
      • load and return layouts adminhtml_sales_order_create_load_block_{block} - update page header title, user name, messages
    • store selection, on selected > loadBlock (header,data) +store_id
      • init session - save store_id to quote session, event create_order_session_quote_initialized
      • processActionData, event adminhtml_sales_order_create_process_data_before, no more post data, nothing to process
      • load and return layouts adminhtml_sales_order_create_load_block_{block} - user name header, data and messages ---- quote saved when customer and store selected, default customer group, inactive
    • data: ... shipping_method - sales/order/create/shipping/method/form.phtml
      • rate.error_message, rate.method_title, rate.method_description, rate.price
      • helper tax.getShippingPrice (including/excluding by config)
      • "get shipping rates" > loadBlock (shipping_method, totals)

!!! can enter products custom price

quote.customer_note -- admin order comments

processData:

  • order create model = model adminhtml/sales_order_create implements Mage_Checkout_Model_Cart_Interface -- same as cart model i getquote i setQuote i addProduct i saveQuote
    • !importPostData - account (customer fields), comment, {billing,shipping} address, payment_method, coupon. quote.setBillingAddress -- updates old one with new data
    • !applySidebarData - add {order,cart,wishlist}_item, add product, remove item, empty cart
    • updateQuoteItems - update qty or order items
    • applyCoupon
    • setAccountData -- customer/form, extract data from request, form attributes collection
    • recollectCart
    • initFromOrder -- for reorder
    • initFromOrderItem -- same
    • moveQuoteItem - to order, cart, wishlist, remove
    • ... shipping method, rates, payment method

edit order

? admin order create

  • customer, store, data inline steps ? admin order edit -- same as create + tweaks
  • session.use old shipping method
  • initFromOrder:
    • session.order id, currency id, customer id, store id
    • event init_from_order_session_quote_initialized
    • add items:
      • only order items from <available_product_types> -- all should be added
      • only items not shipped and not invoiced
      • add items from buy request
      • event sales_convert_order_item_to_quote_item
    • copy fieldset {shipping,billing}Address order -> quote
    • copy fieldset order -> quote
    • event sales_convert_order_to_quote
    • collect rates

save new/edited order

  • _processActionData: -- create addresses, save shipping/payment mehtods, apply coupon, tollect totals and save quote
    • event adminhtml_sales_order_create_process_data_before
    • model adinhtml/sales_order_create.importPostData:
      • set account data
      • quote.customer_note
      • create {billing,shipping} address from data
      • set {shipping,payment} method from data
      • save coupon, mark quote to recollect
    • skip lots of {reset_shipping, collect_shipping_rates, sidebar - applySidebarData, add/move/remove/update product/items}
    • save payment data
    • event adminhtml_sales_order_create_process_data
    • collect totals and save quote
    • skip {add products, update items, apply coupon}
  • set payment data
    • check payment internal, country, currency, min-max, zero total
    • payment.importData -> method.assignData, validate
  • order create model.importPostData -- account, addresses, shipping method and comment
  • order create model.create order
    • prepare customer:
      • for existing, update/create addresses in book
      • for new, generate password, set and validate customer/form data -- attribute data model.validte(value), save addresses
      • save to quote customer_* form.user attributes
    • _validate:
      • check items without errors
      • check method selected, available, validate
    • prepare quote items -- options
    • !if session.order --- editing order
      • remember old id, increment, edit_increment, generate new increment: "{oldincrement}-{editincrement+1}" (e.g. "oldincrement-1", "oldincrement-2")
      • !order.cancel
    • !new order = service.submitOrder
    • link old order with new one
    • event checkout_submit_all_after
  • clear session quote -- customer_id, store_id, quote_id
  1. model adminhtml/sales_order_create.updateQuoteItems:
  • item.setCustomPrice, setOriginalCustomPrice
  1. quote.collectTotals
  2. subtotal quote total collector.collect
  • each items[]._initItem
  • item.calcRowTotal
  • getCalculationPriceOriginal --- will return custom price if defined
  • item.row_total = qty * calculated price original

custom price - saved in DB in quote_item quote.is_super_mode quote_item.getCalculationPrice - custom_price if exists, otherwise convert price

Refund

adminhtml_sales_order_creditmemo_start -> _new _initCreditmemo:

  • by order_id
  • if present, _initInvoice -- just load by invoice_id and order
  • sales/service_order.prepareCreditMemo or .prepareInvoiceCreditMemo
    • convert order_items -> creditmemo_items
    • shipping_amount, adjustment +-
    • creditmemo.collectTotals -- subtotal: subtotal = sum(row total), subtotal_incl_tax = sum(row_total_incl_tax), grand total shipping, tax, discount, grand_total, cost_total, weee
  • set back_to_stock from saved data and config auto return enabled
  • event adminhtml_sales_order_creditmemo_register_before
  • register in 'current_creditmemo'

creditmemo any field = order_item.invoiced - refunded tax proportionally refund qty/all qty

creditmemo.state OPENED,REFUNDED,CANCELED

save: adminhtml/sales_order_creditmemo/save

  • data - items-qty, online/offline, comment, shipping, adjustment +-
  • add comment if defined (text, visible on front, notify)
  • mark refundedRequested, onlineRequested
  • creditmemo.register
    • creditmemo item[].register
    • creditmemo.refund
      • state REFUNDED (if payment reassigns, state can be OPEN)
      • update order.*_refunded
      • invoice.is_used_for_refund, .total_refunded
      • payment.refund(creditmemo)
        • payment.parentTransactionId -- automatically sets capture transaction - invoice transaction id
        • payment.shouldCloseParentTransaction = true -- automatically marks to close
        • method.setStore -- always available, store from order
        • method.processBeforeRefund -- not to extend, sets payment.refundTransactionId
        • method.refund
        • method.processCreditmemo -- can extend, by default creditmemo.transaction_id = payment.last_trans_id
      • event sales_order_creditmemo_refund
    • order.total_online_refunded, total_offline_refunded, total_invoiced_cost
  • _saveCreditmemo -- creditmemo+order in transaction: core/resource_transaction->addObject()...->save()

Cancel Operations

order:

  • cancel order_item invoice:
  • cancel shipment credit memo

product link types:

  • related }
  • upsell }
  • crosssell }
  • grouped

Order States

  • NEW -- just created but worthless, not counted in reports
  • PENDING_PAYMENT -- useful one? used by Nets before order is paid
  • PAYMENT_REVIEW -- fraud etc.
  • PROCESSING -- invoiced or shipped
  • COMPLETE -- invoiced + shipped
  • CLOSED -- fully refunded
  • CANCELED
  • HOLDED -- manually

Mage_Sales_Model_Service_Order:

  • prepareInvoice
  • prepareShipment
  • prepareCreditMemo -- $order, qtys. qty to refund = invoiced - refunded
  • prepareInvoiceCreditMemo

order_item.qty_{ordered,invoiced,shipped,refunded,canceled,backordered}

  • getQtyToRefund = qty_invoiced - qty_refunded
  • getQtyToShip

Invoice.collectTotals when register()

  • totals = sales/order_invoice_config.getTotalModels
  • each total model[].collect()

subtotal -- invoice.subtotal,subtotal,subtotal_incl_tax,grand_total + base discount -- invoice.discount_amount,grand_total + base shipping -- invoice.shipping_amount, shipping_incl_tax, grand_total + base tax -- invoice.tax_amount, hidden_tax_amount, shipping_tax_amount, shipping_hidden_tax_amount + base grand_total -- dummy cost_total -- invoice.base_cost wee -- invoice.subtotal, tax_amount, subtotal_incl_tax, grand_total

Ship Separately

  • by default only bundle implements this
  • product.shipment_type
  • product.weight_type -- 0 dynamic, 1 fixed
  • configurable.getOrderOptions -- together
  • buncle.getOrderOptions -- as saved
  • typical usage: $item->getHasChildren() && $item->isShipSeparately()

affects:

  • quote.getItemsSummaryQty -- if ship together = bundle.qty, if separately = sum(bundle.qty * child.qty)
  • shipping method flatrate -- qty calculation for free boxes
  • shipping method tablerate -- affects package_value and free qty
  • order_item.isDummy -- don't ship irrelevant combinations
  • quote address total shipping -- if separately, add calculate weights (fixed/dynamic)
  • shipping packaging grid -- skip weight for ittelevant lines
  • shipping USA methods -- get all items, count separately

Tracking

track -- per shipment order_shipment.getTracksCollection order_shipment.addTrack track -- carrier_code, track_number, title, description, weight, qty

CUSTOMER

convert customer address custom attributes to order address -- fieldsets? address templates? in <addres_templates> -- text/oneline/html/pdf/js_template

email:

  • new account -- login/password
  • account confirmation -- login/password, link to confirm
  • account confirmed -- just text
  • password reset link
  • your new password is ...

customer/customer:

  • authenticate -- check needs confirmation, check password, event customer_customer_authenticated
  • set/hash/generate/validate/encrypt/decript password
  • sendNewAccountEmail
  • is confirmation required, get random confirmation key
  • getName -- checks prefix,middlename,suffix is visible (customer_eav_attribute)
  • get/add addresses
  • get {primary,default} {billing,shipping} address. customer.default_shipping/billing = {id} -- load address by id
  • getAttributes
  • is in store, get shared {store,website} ids -- only current website stores or all stores
  • validate
  • changeResetPasswordLinkToken, isResetPasswordLinkTokenExpired

customer/group:

  • getTaxClassId

eav_attribute:

  • is_required
  • is_user_defined

catalog_eav_attribute:

customer_eav_attribute:

  • is_visible
  • is_system
  • multiline_count
  • input_filter (e.g. date, datetime)
  • validte_rules
  • data_model

eav/config singleton:

  • getAttribute(entity type, code)
  • getEntityAttributeCodes

editable customer attribute:

  • eav_attribute: attribute_code, is_required, is_user_defined = 1, backend_type (Input Type), frontend_input (), default_value
  • customer_eav_attribute: is_visible (Show on Frontend), input_filter, is_system = 0, validte_rules -- min/max length, input validation

? table customer_form_attribute

adminhtml/custom_attribute_new -> _edit:

  • _initAttribute:
    • customer/attribute -- eav_attribute by entity_type_id = 1
  • store data in session
  • register 'entity_attribute'
  • content block -- enterprise_customer/adminhtml_customer_attribute_edit -- form container, form follows
    • delete button only if not user defined
  • *_edit_form -- dummy
  • left block -- 2 tabs, main content tab
    • parent block eav/adminhtml_attribute_edit_main_abstract (same for edit product attribute etc.) -- form
      • base properties fieldset:
        • attribute_code
        • frontend_input
        • default_value_{text,yesno,textarea}
        • is_unique
        • frontend_class
      • locked attributes from <eav_attributes><{ENITY-TYPE}><{ATTRIBUTE}><locked_fields> -- disabled, readonly
    • enterprise_customer/adinhtml_customer_attribute_edit_tab_main:
      • remove attributes is_unique, frontend_class
      • add own fields ...
      • used_in_forms = options hardcoded in helper enterprise_customer/customer.getAttributeFormOptions
        • checkout_register/customer_account_create/customer_account_edit/adminhtml_checkout
      • event enterprise_customer_attribute_edit_tab_general_prepare_form

adminhtml/custom_attribute_save

helper enterprise_eav -- input_type > backend_type: text - varchar textarea - text multiline - text date - datetime select - int multiselect - varchar boolean - int file - varchar image - varchar

user_defined = 1, is_system = 0 used_in_forms always adds 'adminhtml_customer' events enterprise_customer_attribute_before_save, enterprise_customer_attribute_save

checkout form: only in enterprise template, after fax before password inject customer defined address attributes enterprise_customer/form.setFormCode('') customer/form read from table 'customer_form_attribute' by form_code = 'customer_register_address' and entity_type only user defined and visible attributes (skip image and file) get renderers from block 'customer_form_template' for each input type when form prepare layout

customer, customer address -> init checkout -> quote.assign customer -> quote.assignCustomerWithAddressChange

  • new quote address.importCustomerAddress -- customer default billing address
    • copy fieldset 'customer_address' > 'to_quote_address'
  • same for shipping

when placing order --- only enterprise, in community need add to fieldset manually

copy fieldset 'sales_convert_quote' > 'to_order':

  • copy user defined customer attributes with prefix -- order.'customer_*' copy fieldset 'sales_convert_quote_address' > 'to_order_address':
  • copy user defined address attributes copy fieldset 'sales_convert_billing_address' > 'to_order': copy fieldset 'sales_convert_shipping_address' > 'to_order':

! system > config > customer > account creation > generate human-friendly ID

  • when disabled, customer_id will be always 1,2,3, ... as database handles, increment_id = null
  • when enabled, increment_id is populated like in orders etc.

WIDGETS

widget.xml: My Widget My Widget's description text 1 My Parameter My Parameter Description 1 <sort_order>0</sort_order> something First option 1 Second 2 <source_model>mymodule/source</source_model> <helper_block> mymodule/widget_helper_block </helper_block> value1 value2 value3 mymodule/field_renderer Field renderer... ...instead of type <unique_id> </unique_id> Template Widget Template default.phtml <as_something> Grid mode grid.phtml </as_something> Other mode other.phtml

registered types:

widget/widget.getWidgetsArray widget config xml is cached with CONFIG tag Mage::getConfig()->loadModulesConfiguration('widget.xml', $config);

load options adminhtml_widget_loadoptions:

<adminhtml_widget_loadoption>

  • get block widget/adminhtml_widget_options -- is form
  • read and assign to block 'widget_type' and 'widget_options' -- from selected widget
  • *_options.addFields:
    • widget/widget.getConfigAsJson(type)
      • searches widget.xml for match by type, returns first --- stupid, why not by code
      • work with widget config.parameters
    • for each parameter, _addField:
      • ...

layout updates.display_on:

  • categories anchor -- default,catalog_category_layered
  • categories nonanchor -- default,catalog_category_default
  • all products -- default,catalog_product_view
  • each product type[] -- default,catalog_product_view,PRODUCT_TYPE_{type}
  • all pages -- default
  • specific page

when changing display_on

ajax call <adminhtml_widget_instance_template>

  • init widget instance: set type, package_theme
  • block widget/adminhtml_widget_instance_edit_chooser_template
    • set selected -- what?
    • set widget templates -- widget/widget_instance.getWidgetSupportedTemplatesByBlock -- by selected block ajax call <adminhtml_widget_instalce_blocks>
  • block widget/adminhtml_widget_instance_edit_chooser_block
  • blocks with !!!

save:

selected layouts --- 'page groups' resource widget_instance._afterSave:

  • delete old widget_instance_pages
  • for each 'page_group'[]:
    • save layout updates -- return ids
      • for each update in page_group.layout_handle_updates -- e.g. CATEGORY_4:
        • generate widget_isntance.generate layout update xml -- checks template exists
        • layout update inserts widget as block and contains setData for each parameter
    • save pages and link to core layout updates ids

widget_instance -- type, theme, title, stores, parameter values

widget_instance_page -- block, entities, template widget_isntance_page_layout -- link with generated rows in core_layout_update

all layouts: model widget/widget_instance._layoutHandles, _specificEntitiesLayoutHandles

select page --- getLayoutsChooser: -- all layout handles with !!! -- EXCEPT default, catalog_category_, catalog_product_, PRODUCT_* -- all specific ones block widget/adminhtml_widget_instance_edit_tab_main_layout.getDisplayOnOptions, getDisplayOnContainers block widget/adminhtml_widget_instance_edit_chooser_layout.getLayoutHandles -- all layouts look like specific CATEGORY* or PRODUCT_*

API

api v1 - xmlrpc/soap v1: register api.xml: convert/maincard

api.xml: soap 1 some/class some/model <some_name> 0 Human message, will be translated </some_name> 0 some/acl/path some/model somemethod 0 some/acl/path array <some_name> 0 Something weng wrong </some_name> <title>MyModule</title> <sort_order>0</sort_order> <something_nested translate="title" module="mymodule"> <title>Nested Options</title> </something_nested> <resources_function_prefix>

  </resources_function_prefix>
</v2>

api/server.run

  • initialize - create given adapter by requested code, create given handler and assign to adapter
  • adapter.run
    • zend_xmlrpc.setClass(handler).handle()

api/config -- node tree

  • own cache tag 'config_api' -- Web Services Configuration
  • api.xml
  • getAdapters: ...
  • getActiveAdapters:
    • only 1 and check -- extension_loaded

adapter: xmlrpc/soap/soap_v2/soap_wsi, default -> soap -- deals with request/response, exposes handler methods

setHandler

'xmlrpc' adapter, 'default' handler:

Zend_XmlRpc_Server.setClass(handler).handle()

'soap' adapter, 'default' handler:

construct:

  • _getWsdlConfig - object:
    • name = 'Magento'
    • url = current url
    • handler = unassigned when constructing, so null wsdl requested:
  • read Mage_Api/etc/wsdl.xml as template
  • substitute wsdl content using core/email_template_filter, use wsdl.{name,url,handler} as vars non-wsdl - normal call:
  • instantiate server:
    • ini_set wsdl_cache_enabled yes/no by config
    • build wsdl url -- current or if server vars PHP_AUTH_USER, PHP_AUTH_PW defined, build link like http://name:user@host/...
    • new SoapServer with self ?wsdl, setClass(handler)
  • SoapServer.handle()

handler: default/soap_v2/soap_wsi -- class with exposed methods

  • {start,end}Session, login, call, multiCall, resources, {resource,global}Faults

faults

handler->_fault('code', 'someresource'): -- can use custom message

  • first search resources/someresource/faults, then global faults

session

handler.login(name, key)

  • api/session.login
    • api/user.login
      • authenticate - load by name and check key
      • clean older sessions -- by timeout from System > Config > API > Config > Session Timeout (1 hr default)
      • log login -- increment api_user.lognum
      • record session -- insert/update to api_session
      • event api_user_authenticated
    • check user is active, assigned to some role handler.call(sessId, ...) -- session is always mandatory
  • api/session.isLoggedIn -- loads user from api_session by user_id -- check timeout not passed -- loads ACL -- records login, lognum

ACL

api/config.loadAclResources -- recursively build ACL paths in api/acl/resources/..., e.g. 'catalog/category/attributes' resource api/acl.loadRoles -- [].addRole(role_user or role_group) resource api/acl.loadRules -- tree

call

  • instancofe api/resource
  • for each object row in response, can use _getAttributes:
    • filters _isAllowedAttribute: _ignoredAttributeCodes['global'], _ignoredAttributeCodes[$type]
    • _attributesMap['global'], _attributesMap[$type] -- aliases, e.g. 'order_id' instead of 'entity_id'

SOAP v2

controller Api/controllers/V2/Soap:indexAction -- /api/v2_soap/index adapter, handler 'soap_v2' all resources with suffix *_v2, e.g. sales/order_api -> sales/order_api_v2

adapter 'soap_v2':

  • _getWsdlConfig: -- load from all modules, XML tree, elements of Mage_Api_Model_Wsdl_Config_Element
    • was varien object (name, url, handler), now model api/wsdl_config
      • setHandler (handler code)
      • init:
        • caching enabled/disabled with main CONFIG type
        • no cache tags, to reset -- flush Magento cache
        • load own own wsdl2.xml + load all modules wsdl.xml
        • every loaded file is processed by XML through processFileData:
          • core/email_template_filter.filter -- vars wsdl.{name, url -- current, handler -- code? 'soap_v2'}
  • run?wsdl
    • result tree is printed as XML
  • run normal:
    • no changes, regular {new SoapServer}.setClass(handler).handle()

handler 'soap_v2':

__call magic method:

  • parse joined function to v1 resource name.method using api/v2/resources_function_prefix !need to register by ourselves e.g. customerCustomerInfo -> customer.info (given <resources_function_prefix><customer>customerCustomer</></>)
  • proxies usual ->call()

REST API

Mage_Oauth:

  • register consumers
  • init/authorize/access token steps
  • parse headers on REST calls

Mage_Api2 - REST

  • cache enabled if config cache enabled
  • no events
  • to add api type, rewrite api2/server and extend _apiTypes

Tables

api2_acl_user -- assign admin user to admin role api2_acl_role -- #1 == guest, #2 == customer, others - admin roles. create, updated, role name api2_acl_rule -- role-resource-operation api2_acl_attribute -- user type-resource-operation-attributes[] joined

  • users, role, access to attributes

to edit ACL in admin, resource groups (endpoints) + privileges (actions)

Access to endpoints / per operation / per role - 2 admin roles CAN have different endpoint access

desctibed by api2.xml > resources/privileges/{user type}/{operation} guest - only one role, only available:

  • product.read
  • category.read
  • product image.read customer - only one role , available above +:
  • orders.read
  • order items.read
  • order addresses.read
  • order comments.read
  • customer.read/update
  • customer address.* admin - many roles, admin user can be assigned only to one:
  • all methods

Access to attributes / per user type / read or write - 2 admin roles CANNOT have different attribute access

config.xml: something module/model

api2.xml: <resource_groups> </resource_groups> <title></title> <working_model></working_model> 1 <action_type></action_type> 1,2,... Human Name <exclude_attributes> 1 </exclude_attributes> <include_attributes> 1 </include_attributes> <entity_only_attributes> 1 </entity_only_attributes> <force_attributes> 1 </force_attributes>

resource_groups? resource/{type}/group? resource/{type}/privileges?

Accept: something/specific Accept: something/* Accept: /

Implemented response renderers:

  • application/json -- default, matches / -- json_encode
  • text/plain -- query style, http_build_query
  • text/xml == application/xml == application/xhtml+xml

Headers:

Authenticate -- for oauth_adapter.isApplicable Version -- request.getVersion

Entry point

/api/rest --> /api.php?type=rest match ?type supported in api2/server._apiTypes = rest api2/server.run:

  • renderer = api2/renderer::factory by request.getAcceptTypes -- sorted by requested weights
    • search global/api2/response/renderers to match adapter.type = Accept: type
  • authenticate -- assigns server.authUser - instanceof api2/auth_user{guest/customer/admin}. getLabel, getRole.
  • _route -- collect routes, run each route.match -- set request.model
  • _allow -- check acl
  • _dispatch
_authenticate

api2/auth.authenticate:

  • for each registered auth adapter .isApplicableToRequest, .getUserParams . return (type=admin/user/guest, id) -- api2/auth_adapter_oauth -> applicable if header 'Authorization', getUserParams oauth/server.checkAccessRequest
  • returns user model by type
_route
  • first match hardcoded route api2/route_apiType -- matches 'api/:api_type'
    • returns api_type value = rest
    • request.setPathInfo -- remove api/:api_type, leaving e.g. /customers
    • request.setParam api_type = rest
  • config.get routes by api_type=rest -- 27 routes
    • load from api2/resources/routes
    • wraps api2/route_{type=rest} for each one. route => 'customer/:customerId' defaults => { model => 'customer/api2_customer' -- from api2/resources/ type => '$resourceKey' -- request.getResourceType action_type => 'entity' -- or 'collection' }
  • api2/router.setRoutes.route
    • for each route api2/route_{type=rest}.match
      • returns matched {names? + wildcards + defaults}
    • when matched, request.setParams -- from match return
    • match must return params (request.setParams), set request.resourceType and request.model
_allow
_dispatch
  • load resource model (:resource_:api_:user_v:version) :resource = model, e.g. customer/api2_customer :api = api type, e.g. rest :user = user type, i.e. guest/customer/adin :version - requested or first available -- {resource model}rest{guest/customer/admin}v1, e.g. {customer/api2_customer}{rest}_{customer}_v{1}
  • model.set request,response,api user
  • model.dispatch -- api2/resource.dispatch
    • switch action_type(from route config)+operation(depend on http verb) -- {entity/collection},{retrieve/create/update/delete}
      • collection.create - <action_type>collection</action_type>,POST -- _create/_multiCreate depending on data
        • filter data via api2/acl_filter
        • _create -- do actual stuff here and return new item location
      • collection.retrieve
        • _retrieveCollection ...
    • _render:
      • response.setMimeType = renderer.getMimeType
      • response.setBody = renderer.render(data from resource method)

! customer-specific resource models just add filter to collection entity_id = apiUser.userId

filter collection - ?filter[0][attribute]=name api2/route_rest.match -- Zend_Controller_Router_Route

api2/acl_filter:
  • in -- filter allowed attributes to write
  • out -- filter allowed attributes to read, leave only requested -- ?attrs=first,last or ?attrs[]=first&attrs[]=last
  • collectionIn
  • collectionOut

api2/response -- zend http response + messages[] - {message,code} api2/request - zend http request +:

  • order field/order direction/page number/page size
  • action_type, ... other

resource model:

  • _create
  • _multiCreate
  • _retrieve
  • _retrieveCollection
  • _update
  • _multiUpdate
  • _delete
  • _multiDelete
  • getAvailableAttributes(user type, operation)

ENTERPRISE

  • target rules?
  • reward points?
  • website restrictions?
  • full page cache?
  • payment bridge?

Target Rules -- product relations rules

admin conditions catalog related, upsell checkout crossell

depends on Mage_Rule heavily - conditions, actions, customer groups, website ids

tables

enterprise_targetrule

  • name, date from/to, type(related/crossell/upsell), conditions, actions -? actions select, actions select bind targetrule_customersegment -- many to many targetrule_product -- products matching rule condition enterprise_targetrule_index --- for given (type,product,store,customer_group) find customer segments ?enterprise_targetrule_index_{related,upsell,crosssell} ?enterprise_targetrule_index_{related,upsell,crosssell}_product -- many to many

Index

targetrule indexer is not visible resource indexer delegates to sub-indexers 'index_{related,upsell,crosssell}'

cron runs every hour: -- Check store datetime and every day per store clean index cache

  • for each website, only at local midnight, indexer.logEvent - targetrule clean targets --- what does it do? prepare to dataobject = {type_id = null, store = []}
  • indexer.registerEvent - targetrule clean targets -- process all matched above? -- what does it do?
    • cleanIndex({type_id = null, store = :id}) -> delegate to all types 'index_{type}'.cleanIndex
      • deletes records from 'index_related' after product save:
  • dispatch global reindex: targetrule_product.reindex_targetrules

after saving product, reindex: index._reindex({id, store_id, rule, from_date, to_date})

  • for each type, remove index by product
  • remove matched product-rule associations from targetrule_product (see default mage_rule.unbind...)
  • for each rule, if product matches condition, insert into targetrule_product (default mage_rule.bindRuleToEntity)

after saving rule, populate targetrule_product -- all matching condition products

typical exampls:

related:

  • filter: some category
  • display related: from same category and same attribute set and same gender upsell:
  • filter: some category
  • display upsell: from same category and price > 100% of matched

Related

Default

<catalog_product_view> catalog/product_list_related

  • itemsCollection = product.getRelatedProductCollection
    • link instance.get product collection -- all products linked to current
  • exclude items in cart
  • don't use category id when building URL
    1. model catalog/url._getCategoryIdForUrl <- catalog/url.getProductUrl <- catalog/url.getUrl
  • ! no limit
  • add checkbox to add to cart if can - not composite, no required options, saleable

EE - on product view page replace related, upsell

replaces block catalog.product.related renders each item in separate block catalog.product.related.item -- contents the same + Caching

targetrule_index -- true/false if generated for given product/customer segment targetrule_index_{type} -- true/false if generated for given product/customer segment/type targetrule_index_{type}_product -- actual data! generated and saved on demand

on first request type.getProductIds for missing customer segments, generate, save and mark generated by flag=1

  1. resource index._matchProductIdsBySegmentId for each rule[]:
    • ensure date from, to matches
    • resource index._getProductIdsByRule
      • rule.getActions().getConditionForCollection is cached in targetrule table -! do actual search of match by PRODUCT-RULE-SEGMENT
  2. type index.saveResultForCustomerSegments - check targetrule_index_{type}. if exists, meaning already created, delete {type}_products and save new ones - if not exists, not generated, insert row and save new ones into {type}_products
  3. resource index.saveFlag on next requests, return directly from renerated type index
  4. type index.loadProductIdsBySegmentId -- select directly from targetrule_index_{type}

resource targetrule/index.getProductIds

getItemsCollection:

  • depending on behavior, add rule-based and custom items:
    • get link products -- same logic wrapped far far away
    • get target rule products
      • exclude current product -! index resource.getProductIds by (type,product,exclude)
        • get current customer segments 0,30,32
        • find matched customer segments in index table by (type,product,store,customer group)
        • for each current customer segment:
          • if main index has segments for this customer segment[], return type index.loadProductIdsBySegmentId -- load extended version?
            • select from targetrule_index_{related,upsell} by (product,store,customer_group,customer segment)
          • if main index has NO segments for this customer segment[]:
            • type index._matchProductIdsBySegmentId
            • type index.saveResultForCustomerSegments
            • save flag

Reward Points

  • how to handle customer balance?
  • customer balance history?
  • quote columns for discount?
  • where input injected on cart/checkout?
  • can redeem part?
  • admin use points?

expiry date:

  • static -- each balance increase has own expiration
  • dynamic -- expiration date prolonged on each balance increase

actions:

  • purchase -- on event checkout_submit_all_after?
  • registration --
  • newsletter
  • review
  • tag
  • convert invitation to customer
  • convert invitation to order

! rate currency->points must be defined ! currency->points: -- works only for whole increments, not proportionally! for each $1 you earn 1pt, order for $10 = 10pt for each $10 you earn 5pt, matches only when total >$10. $10 = 5pt, $15=5pt, $20=10pt ! when credit memo, can set manually Refund creditmemo amount

$order->setForcedCanCreditmemo(true); $creditmemo->setAllowZeroGrandTotal

tables

enterprise_reward - customer balance per website reward_history -- new balance and change details - action, entity, delta, used, voided, expired -- in $$ and points reward_rate -- per website/customer group/direction reward_salesrule -- cart price rule - reward points

attributes:

quote.use_reward_points, reward_points_balance, {base_,}reward_currency_amount order.reward_points_balance, {base_,}reward_currency_amount, reward_salesrule_points, reward_currency_amount_{refunded,invoiced}

  1. when action happens - add bonus
  2. totals collector for quote, order, credit memo
  3. cronjob every hour:
  • notify before expiring points
  • expire points -. pdf totals

rates

currency to points - when purchase $$, you get points

  • only whole points!
  • when big currency points assigned for each reached step, e.g. $100 - 100pt -- you won't get you 100pt before you reach $100 points to currency
  • your points are worth $$ discount

typical: buy $1 get 1pt -- normal point values exchange 1pt for $0.01 -- small currency values

reward some points

reward.updateRewardPoints: (customer_id, store, action, action entity)

  • reward.canUpdateRewardPoints
    • action instance.set{action,reward,history,entity}
    • action instance.canAddRewardPoints -- check here!
  • reward.save before save: -- update points balance - load by customer -- update existing reward if can - _preparePointsDelta: delta = action instance.getPoints - _preparePointsBalance: - balance += delta, but no more than max points config. - if capped, save cropped points as well! after save: -- update history, including currency balance - prepareCurrencyAmount: - currency{delta,amount} = _convertPointsToCurrency - get rate by direction.calculateToCurrency - e.g. direction = 1 (to currency), 4 points = 1 USD. 10 points = 10/4 = 2.5 USD - history.prepareFromReward - history.*, comment - history.additional data - rate details, cropped points - send balance notification

enterprise_reward/action_abstract

has models reward, reward history, entity (is passed) 13 action types by default responsibilities:

  • canAddRewardPoints -- extend
    • checks history if not duplicate by entity and action
    • checks applied limit exceeded
  • getPoints -- extend; how much to add! used when ading and to estimate in tooltip
  • getRewardLimit -- extend; max applied qty in history
  • getHistoryMessage -- extend
  • isRewardLimitExceeded
  • estimateRewardsQtyLimit

tooltips - you'll get X if you do Y

block tooltip

  • initRewardType(action instance) -- in layout
  • before html - _prepareTemplateData:
    • 'reward_points' = reward.estimateRewardPoints
    • if has exiting record, add also current balance in points and currency

redeem points

  • when saving payment method, event sales_quote_payment_import_data_before
  • if balance more than min, quote.setUseRewardPoints = true -- to be used in collector
  • total_quote_reward.collect:

after order placed: balance deducted

add custom reward

add action and model -- static reward::setActionModelClass - (action id, model)

// use only money customer spend - shipping & tax
$monetaryAmount = $quote->getBaseGrandTotal()
    - $address->getBaseShippingAmount()
    - $address->getBaseTaxAmount();
$monetaryAmount = $monetaryAmount < 0 ? 0 : $monetaryAmount;

order states visible on front

global/sales/order/states <visible_on_front></visible_on_front>

predispatch event

1 way - REPLACE ROUTE: $request->setModuleName('restriction') ->setControllerName('index') ->setActionName('stub') ->setDispatched(false); 2 way - REDIRECT: $response->setRedirect($url); $controller->setFlag('', Mage_Core_Controller_Varien_Action::FLAG_NO_DISPATCH, true);

Website Restrictions

restrict store - require logged in? whitelist some CMS pages? whitelist some routes?

check customer.isLoggedIn -- work on events predispatch before? customer/account/create - disable if configured -- predispatch?

restriction mode - website closed

replace any route with full-page landing. no login or signup how?

controller action predispatch event:

  • event websiterestriction_frontend -- can 1) disable restriction or 2) mark user logged in

  • website closed - ALLOW_NONE - replace all routes with 'restriction/index/stub'

    • cache per website
    • load CMS page by config
    • apply design package/theme (from-to date support) if enabled in CMS page
    • render CMS page
  • ALLOW_REGISTER, ALLOW_LOGIN:

    • whitelist routes in config/frontend/enterprise/websiterestriction/full_action_names/generic/*
    • if registration is enabled, whitelist routes in config/frontend/enterprise/websiterestriction/full_action_names/register/*
      • see event customer_registration_is_allowed:
        • website restriction can disable if enabled and not ALLOW_REGISTER
    • mode 302 redirect to landing - whitelist only given cms page view and redirect
    • non-whitelisted URL - redirect to login

    allow none - nothing works, always stub allow login or register - redirect to landing or login

    add layout handle <restriction_privatesales_mode> only for ALLOW_REGISTER, ALLOW_LOGIN

    • can use to display widget instance on 'page' mode

FULL PAGE CACHE

placeholder processor subprocessor

mymodule/processor mymodule/processor

cache.xml: mymodule/dynamic_block SOME_CODE <cache_lifetime>86400</cache_lifetime>

Stages:

  1. app/etc/enterprise.xml: <request_processors> -- model processor
  • mage::run() -> app.run() -> cache.processRequest -> enterprise_pagecache/processor.extractContent
  1. render cold page:
  • after rendering dynamic blocks a. save rendered dynamic blocks to cache -- own cache lifetime and tags. To be loaded from cache in applyWihoutApp! b. wrap it in start/end tags content
  • before final response in controller_front_send_response_before, extract dynamic blocks to save main page in cache without dynamic data
  1. render warm page:
  • load base cache
  • render and substitute all placeholders (render definitions) without app
  • if not all rendered without app:
    • replace request.{moduleName,controllerName,actionName} = pagecache/request/process
    • save request.routingInfo - aliases, requested_{route,controller,action}
    • pagecache/request/processAction -- container[].applyInApp -- process rest containers that need app

Load Page:

processor.extractContent:

  • load cached by request id
  • _processContent: -- process containers
    • _processContainers -- container[].applyWithoutApp, if false, then return for further processing in app
    • replace form key (cached as FORM_KEY_MARKER)
    • replace session id (cached as SID_MARKER)
    • if all containers processes without app, we are done! return content --> to be returned to app.run before init modules
    • if unprocessed containers left, replace request routing info, return false -- no direct response, normal magento flow ... -- run our controller to replace containers in app

if left containers, pagecache/request/processAction: container[].applyInApp(): -- cached page content as parameter

  • _renderBlock -- place custom logic here!
  • _applyToContent(cached page content, block content) -- just replace definition in cached with rendered value. start/end tags kept!
  • if processor.subprocessor: -- won't save rendered to cache if missing!
    • subprocessor.replaceContentToPlaceholderReplacer -- replace nested rendered blocks with definitions by kept start/end tags
    • save rendered block to cache -- personal tags and lifetime

Processor - singleton Enterprise_PageCache_Model_Processor

isAllowed - deny if:

  • COOKIE['NO_CACHE']
  • GET['no_cache']
  • GET['SID']

canProcessRequest - above + following:

  • check max depth -- number of GET params
  • multicurrency ...

_createRequestIds (concat):

  • path
  • cookies:
    • store
    • currency
    • customer group - 'CUSTOMER_INFO'
    • customer logged in - 'CUSTOMER_AUTH'
    • customer segment - 'CUSTOMER_SEGMENT_IDS'
    • user allowed to save cookie
  • design package

Subprocessor - Enterprise_PageCache_Model_Processor_Default

  • getPageIdWithoutApp
  • replaceContentToPlaceholderReplacer

Request Processor -- declared in config per current route

instanceof processor_default

  • allowCache

Placeholder -- just keeps definition

in constructor accepts string block definition key=value (container, block, cache_id, cache_lifetime + cache key info!) splits definition and saves in _attributes

  • getAttribute
  • get{Start,End}Tag
  • getPattern -- to match and replace containers

Container -- extend

  • _getCacheId -- must extend! default false = completely removed from cached page
  • saveCache
  • _saveCache
  • applyWithoutApp -- extend. default try to load from cache
  • applyInApp -- extend. default use _renderBlock
  • _renderBlock -- extend! custom logic when in app. getLayout->getBlock->toHtml should dispatch event render_block
  • _applyToContent(content, rendered) -- just replace definition in cached with rendered value

Notes: placeholder is created in pagecache/config.getBlockPlaceholder. container has access to placeholder it was created from, e.g.:

<!--{CONVERT_CERT_DYNAMIC
  container="Convert_Cert_Model_Cache_Container_Dynamic"
  block="Convert_Cert_Block_Dynamic"
  cache_id="c1edfcf3858cef371e7eca712834ec17a50c6443" -- block.getCacheKey()
  template="convert/cert/dynamic.phtml"
  something="yes" xyz="33"  -- from original block.getCacheKeyInfo
}-->

Events

controller_action_predispatch_before -- disable standard block cache core_block_abstract_to_html_after:

  • collect all unique blocks cache tags:
    • normal blocks - processor.requestTags[]
    • blocks inside dynamic placeholder blocks - context block.addCacheTag
  • renderBlockPlaceholder:
    • block definition in place of dynamic content -- contains values of block->getCacheKeyInfo! -- when later rendering in app in _renderBlock, can use $block->getAttribute

after model saved/deleted, validator.cleanEntityCache by model.getCacheIdTags == model.getId() + model._cacheTag clean caches after category change (is active, include in menu)

PAYMENT BRIDGE

  1. You install a payment bridge web-application on a separate server. Magento will communicate with it.
  • unified interface for all operations
  • separate server must be PCI compliant
  • how does it handle different methods?
  • how to implement more methods?
  1. ? 3rd-party payment applications accept and store credit cart info. -> you get token worth payment info.

Cardinal Centinel 3D Secure:

  • Authorize.Net
  • PayPal Payments Pro
  • PayPal Payments Pro Payflow Edition (UK only)
  • PayPal Payflow Pro

authorize.net CIM - Customer Information Manager - tokenize payment info

  • token === hash of credit card, can bill many times
  • token can include billing and shipping info

New merchant has successfully been added. Data Transfer Key generated: 564546f3171538385c7bd61c299e6618 Merchant Key generated: 2b4b3d16d8b96c966014b6ea6ceb8bcb72c138ff61c9b477b3cd77f21d5d88b8

payment bridge instance - standalone cc -> payment bridge method -> concrete method

Payment Bridge Instance -- standalone

model pbridge/payment_method_pbridge < payment/method_abstract

Payment Bridge Method Instance - Authorize.net

model pbridge/model_payment_method_authorizenet < pbridge/model_payment_method_abstract -- all Pbridge methods here. (assignData, validate, ...)->proxy to Payment bridge instance < payment/method_cc -- save cc details < payment/method_abstract -- order/authorize/capture/cancel/void/....

block payment iframe used for:

  • payment form -- all payment methods forms extend here
  • payment review
  • payment profile?
  • admin: edit payment

Steps

1. render _formBlockType in iframe, init bridge session

_formBlockType - enterprise_pbridge/checkout_payment_abstract -- define iframe source URL < pbridge/iframe_abstract -- add child block 'pbridge_iframe' in template for form block, include iframe:

  • getSourceUrl: .../bridge.php with encrypted params:
    • !action = GatewayForm
    • locale, order increment id (reserved)
    • payment action = authorize
    • amount, currency
    • redirect url = enterprise_pbridge/pbridge/result
    • customer id, name, billing/shipping addresses
  • fill in credit card numbers: POST bridge.php -? saves card numbers somewhere, generates token
    • redirects to enterprise_pbridge/pbridge/result with encrypted data. token?
    • magento is inside iframe (domain now same as parent):
      • create hidden inputs with values under selected pbridge method: -! token = efaefd79223bdf1970c276637289f5cc
        • original_payment_method = authorizenet
        • cc_last4 = 1111
        • cc_type VI
        • x_params
      • calls parent js class payment.save - submits /onepage/savePayment with public info

2. onepage/savePayment - assignData, validate

assignData -> getPbridgeMethodInstance.assignData:

  • save to info_instance: cc_last4, cc_type
  • save pbridge_data (token, ori...) to info_instance.additionalData
  • save token to pbridge session validate -> getPbridgeMethodInstance.validate:
  • ensure token is saved in additional data

3. after order placed: validate, initialize/order/authorize/capture

assignData validate validate authorize -> getPbridgeMethodInstance.authorize:

  • build request:
    • country
    • order id
    • !magento_payment_action = 'authorize' (can also be 'authorize_capture')
    • !payment_action = place
    • amount, currency
    • client_identifier = hash(quote id)
    • customer email, billing address, shipping address
    • cart items -- reuse paypal/cart
    • notify url -- /enterprise_pbridge/PbridgeIpn
  • api.doAuthorize
    • !api._prepareRequestParams: 'token', 'action' = 'Payments'
    • _importResultToPayment: set payment.transaction_id, prepared message for transaction with original transaction id
    • add response data to info_instance (order_payment)

Payment Bridge App

bridge.php Varien_Bridge.init.run !requested action = from request 'action' action class = Varien_Bridge_Action_Action{requested action} Varien_Bridge_Action.runAction(action class):

  • check allowed IP
  • action class.execute

Gatewayform.execute:

!gatewayCode = from request 'request_gateway_code' gatewayConfig = merchant.getGateways(code):

  • read from db config
  • merge config:
    • merchant - from db?
    • !file config (cfg/payments.php 'payment_gateways'): title, class, active, ... gateway = create from config 'class', e.g. Varien_Payment_Gateway_AuthorizeNet add form fields to config -- from gateway.getFormFields

Payments.execute:

payment action = from request 'payment_action': place/capture/refund/void/cancel/accept/deny/fetch -> proxy to Varien_Payment -> some logic, proxy to current gateway

Varien_Payment_Gateway_Abstract

$_code getFormFields authorize capture void refund acceptPayment/denyPayment fetchTransactionInfo is3DSEnabled ...

TODO

$_COOKIE['currency'] ?

Varien_Http_Adapter_Curl

single-store mode:

  • affects EAV attributes saving for catalog, store_id is always 0

CACHING

Model Caching

cleanModelCache:

  • clean cache ty tags getCacheTags: -- all blocks that depend on this model will be refreshed
    • model._cacheTag, e.g. catalog_category -- refresh all blocks that depend on any category
    • getCacheIdTags -- for each _cacheTag, adds {tag}_{id} -- all blocks that depend on specific category
    • e.g. [catalog_category, catalog_category_1]

block caching

getCacheKey -> getCacheKeyInfo -- variations block depends on, e.g. if customer logged in and current category --> cache.save(data, id) getCacheTags -- internal data block depends on, e.g. when category is updated, content should be refreshed

  • to add tags, there's no internal property, call addCacheTag
  • to add dependency on specific model, e.g. category, call addModelTags -- e.g. will add tag catalog_category_1 block data is saved to cache with tags it depends on at the same time, same tags are saved to cache with themselves as dependency. wtf?

block:

  • data(block::CACHE_TAGS_DATA_KEY)
  • getCacheKeyInfo() -- extend. different behavior for different pages/customers/situations? put variations here
  • getCacheKey() - builds key using getCacheKeyInfo
  • getCacheTags() -- extend or use addCacheTag. put here all tags you depend on. changing one of them will refresh this block
  • addCacheTag()
  • addModelTags($model) -- if you depend on specific model, use this to clean cache when it updates

EAV

attribute backend? attribute frontend? dropdown types? eav resource? eav collection? setup class?

?all attributes = Mage::getSingleton('eav/config')->getEntityAttributeCodes($this->getEntityType(), $object); resource = singleton

product->getAttributes()

Entity - Mage_Eav_Model_Entity_Abstract - loads and saves single item

extends Mage_Core_Model_Resource_Abstract -- which is stupid, only transactions and _prepareDataForTable

  • getEntityType: child class must call setType, will trigger eav/config.getEntityType() -- load from eav_entity_type
  • load:
    • load row from entity table without any attributes, assign to object
    • loadAllAttributes:
      • eav/config.getEntityAttributeCodes(entity type, object) ----- from customer_eav_attributes??? not eav_attribute.entity_type_id???
      • getDefaultAttributes
      • for all - getAttribute/addAttribute -- pupulates _attributeBy{Id,Code,Table}
    • _loadModelAttributes: -- loads and assigns values
      • groups all attributes by select from each table: select all from _varchar by customer id, from _int by customer id etc.
      • union selects from all tables :) _varchar UNION _int UNION _datetime
      • for each loaded attribute id and value:
        • object.setData(code, value)
        • attribute.backend.setEntityValueId -- backend knows that for customer 23 varchar attribute row id is 73847. used later in collectSaveData
    • _setOrigData
    • _afterLoad: every attribute[].getBackend.afterLoad
  • save: -- just compare orig/new data and do insertDuplicate/delete grouped by table
    • delete if isDeleted
    • if marked partialSave, loadAllAttributes -- unused
    • _beforeSave
      • all attribute[].backend.beforeSave
    • _collectSaveData -- return ['newObject', 'entityRow', 'insert', 'update', 'delete']
      • detects static fields, compares new data and old data
      • entityRow -- _prepareStaticValue. all static fields regardless if changed or not. decimal type is locale processed
      • decide on delete[]/update[] if attribute existed in original data
      • insert[] non-empty new value
    • _processSaveData: -! object field value can be Zend_Db_Expr
      • if entity_id defined, loads to ensure exists (of would need to insert with id)
      • insert/update entity row depending on entity id
      • for all collected insert/update/delete attributes, do _insertAttribute / _updateAttribute / _deleteAttributes -- just remembers to future execution
      • _processAttributeValues -- executes insertDuplicate/delete
    • _afterSave
      • all attribute[].backend.beforeSave
  • loadAllAttributes:
    • asks attribute codes from eav/config.getEntityAttributeCodes
    • inits all attributes via getAttribute -- populating _attributesBy{Id,Code}
    • special handling for default attributes, if don't exist in EAV, their attribute model is created in memory with fixed params
  • getDefaultAttributeSourceModel
  • getAttribute -- creates attribute model
  • addAttribute -- register in _attributesBy{Id,Code,Table}
  • _getDefaultAttributes -- extend
  • getAttributesByCode
  • setNewIncrementId

Attribute - Mage_Eav_Model_Entity_Attribute_Abstract

-- entityType.attribute_model from DB

  • isStatic
  • setAttributeModel -- why? attribute instance already created by entity_type.attribute_model
  • getBackend
    • as assigned in eav_attribute.backend_model
    • attribute._getDefaultBackendModel -- extend, special cases for created_at, updated_at, store_id, increment_id
  • getFrontend
    • as assigned in eav_attribute.frontend_model -- do this
    • attribute._getDefaultFrontendModel -- extend, not used
  • usesSource -- only frontend_input == 'select', 'multiselect' OR source_model non-empty
  • getSource
    • as assigned in eav_attribute.source_model
    • attribute._getDefaultSourceModel -> entity.getDefaultSourceModel -- extend, all catalog entities use eav/entity_attribute_source_table - select all options from DB
  • setAttributeModel

Attribute Backend - Mage_Eav_Model_Entity_Attribute_Backend_Abstract

lazy loaded on first attribute.getBackend setAttribute -- always has attribute it works with afterLoad beforeSave setEntityValueId isStatic == attribute.isStatic

Attribute Frontend - Mage_Eav_Model_Entity_Attribute_Frontend_Abstract

  • getValue(object) -- extend
    • if frontend_input is 'select' or 'boolean':
      • attempts to load attribute.source.getOptionText
      • or gets value from falback entity_attribute_source_boolean -- yes/no
    • if frontend_input is 'multiselect' (value is comma-separated), gets source.getOptionText and joins 'value1, value2'
  • getLabel
    • returns attribute.frontend_label or falls back to attribute_code
  • getOption(optionId)
    • delegates to attribute.source.getOptionText

Attribute Source - Mage_Eav_Model_Entity_Attribute_Source_Abstract

getAllOptions -- returns []['label', 'value'] getOptionText -- text by id getOptionId -- by both id or label getIndexOptionText -- for search indexing

!default source model eav/entity_attribute_source_config:

  • _configNodePath -- extend and options will be taken from config node

Entity Type - Mage_Eav_Model_Entity_Type

getEntityIdField getAttributeModel -- e.g. customer/attribute

Entity Collection - builds select

  • addAttributeToFilter('name', ['in' => ['One', 'Two']])
  • addAttributeToSelect('*')
  • load(true) - print result SQL

Attribute Collection - read attribute definition ('entity_attribute')

Mage_Eav_Model_Resource_Entity_Attribute_Collection - extends plain resource of course has children that join additional table (customer_eav_attribute, catalog_eav_attribute) or form in _initSelect

Setup - Mage_Eav_Model_Entity_Setup

Config - singleton Mage_Eav_Model_Config

-getAttribute: -- returns attribute model

-getEntityType(code):

  • creates new eav/entity_type.loadByCode(code)

-getEntityAttributeCodes

  • respects store and attribute set - read from object argument
  • used by entity when loading object
  • _initAttributes: ?? study more?
  • reads from eav_attribute + eav_entity_attribute (or customer_eav_attribute) table by entity type, joins additiaonl table?
    • creates models for all attributes, returnes codes

Entity Type - Mage_Eav_Model_Entity_Type

getEntityAttributeCollection getAdditionalAttributeTable -- catalog_eav_attribute, customer_eav_attribute _load(attribute) getAttributeModel -- by default eav/entity_attribute; can also be e.g. customer/attribute

Database Schema

eav_entity -- deprecated
eav_entity_attribute -- has data. why?
eav_entity_{type} -- deprecated

customer_entity
customer_entity_{type}
customer_eav_attribute -- additional table

catalog_product_entity
catalog_product_entity_{type}
catalog_eav_attribute -- additional table

! \Mage_Eav_Model_Resource_Attribute_Collection::_getEavWebsiteTable
customer_eav_attribute_website

Mage_Eav_Model_Resource_Attribute_Collection::_initSelect joins additional table and attribute scope

Scope Implementation

\Mage_Catalog_Model_Resource_Abstract::_canUpdateAttribute

if ($result &&
    ($attribute->isScopeStore() || $attribute->isScopeWebsite()) &&
    !$this->_isAttributeValueEmpty($attribute, $value) &&
    $value == $origData[$attribute->getAttributeCode()] &&
    isset($origData['store_id']) && $origData['store_id'] != $this->getDefaultStoreId()

\Mage_Catalog_Model_Resource_Abstract::_insertAttribute

TIPS

get all options:

attribute = eav/config.getAttribute(customer, first_name)
attribute.getSource.getAllOptions

or see eav/entity_attribute_source_table that is default source model for product attributes so any attribute of entity_type catalog_product attribute.getFrontend.getAllOptions will load from DB:

$collection = Mage::getResourceModel('eav/entity_attribute_option_collection')
    ->setPositionOrder('asc')
    ->setAttributeFilter($this->getAttribute()->getId())
    ->setStoreFilter($this->getAttribute()->getStoreId())
    ->load();
$this->_options[$storeId]        = $collection->toOptionArray();

Get entity type ID by code:

_entityTypeId = Mage::getModel('eav/entity')->setType(Mage_Catalog_Model_Product::ENTITY)->getTypeId();

catalog attribute:

  • model Mage_Catalog_Model_Resource_Eav_Attribute
  • resource model Mage_Catalog_Model_Resource_Attribute

All catalog entities extend from mage_catalog_model_resource_abstract -- adds extra rules

  • support catalog_eav_attribute.apply_to catalog attributes can apply to specific product types. if not, entity skips attribute
  • support store_id and single-store mode

Default attribute models:

source:

  • eav/entity_attribute_source_abstract
  • _config -- extend, load from config node
  • _store -- load stores from DB
  • _table -- load all options from DB

backend:

  • eav/entity_attribute_backend_default = abstract
  • _datetime -- normalize to format yyyy-MM-dd HH:mm:ss
  • _array -- join(', ', values)
  • _serialized

frontend:

  • eav/entity-attribute_frontend_default = abstract
  • _datetime -- ISO_8601, date('c'), 2004-02-12T15:19:21+00:00

Eav Setup - Mage_Eav_Model_Entity_Setup

  • getEntityTypeId

  • {get,add,update,remove}EntityType

  • {get,add,update,remove}AttributeSet

  • {get,add.update,remove}AttributeGroup

  • get{AttributeSet,DefaultAttributeSet}Id

  • get{AttributeGroup,DefaultAttributeGroup}Id

  • _prepareValues -- extend, defaults for addAttribute

  • getAttribute

  • addAttribute -- if exists, will updateAttribute

  • updateAttribute

  • removeAttribute

  • addAttributeOption

  • getAttributeTable

  • addAttributeTo{Set,Group}

  • getDefaultEntities -- extend

  • installEntities -- supply of used getDefaultEntities

    • []: addEntityType, addAttribute, setDefaultSetToEntityType
  • createEntityTables -- has bug here?

    singleton klarnacheckout/observer apiCopyCollectionPaymentReservationNumber

Quote Address

getAllItems() -- see property _nominalOnly. null - all, true - nominal, false - non-nominal -- see quote_item.isNominal == item.product is recurring getAllNonNominalItems() -- getAllItems + property _nominalOnly = false getAllNominalItems() -- getAllItems + property _nominalOnly getAllVisibleItems() importCustomerAddress -- using fieldset exportCustomerAddress -- to customer address importOrderAddress -- when editing order? toArray - rates, items, totals

Rule Condition

-- model rule/condition_abstract implements Mage_Rule_Model_Condition_Interface

  • asHtml: type + element + operator + value + removeLink + chooserContainer

rule/condition_abstract > rule/condition_product_abstract

salesrule/rule_condition_product -- adds attributes {Qty,Price,Row total} in cart enterpise_targetrule/rule_condition_product_attributes ....

API

sales_order_shipment:

  • items
  • info
  • create -- supports individual qty - part shipment
  • {add,remove,info}Track -- sales/order_shipment_track
  • getCarriers -- only with isTrackingAvailable
  • sendInfo -- shipment.sendMail
  • addComment

sales_order_invoice:

  • items
  • info
  • create -- supports itemsQty - part invoice
  • capture -- whole invoice
  • void -- whole invoice
  • cancel -- whole invoice
  • addComment

sales_order_creditmemo:

  • items
  • info
  • create -- supports qtys, store credit amount
  • cancel -- whole
  • addComment -- sales/order_creditmemo_comment

Filter Products

visibility catalog/search:

Mage::getSingleton('catalog/product_visibility')->addVisibleInCatalogFilterToCollection($collection);

enabled:

$product->isVisibleInCatalog()

product collection.addAttributeToSelect(model catalog/config.getProductAttributes)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment