This page details a new 'thick' API for a virtual order entity. The interface to the API exposes basically a contribution and its line_items. The API manages the creation of subsidiary objects when the order is created, ie memberships, participants, and/or pledge payments. It also looks after ensuring all appropriate entries are created in the financial_item, financial_trxn, and entity_financial_trxn tables as per CiviAccounts Data Flow. (NB: this API has previously been discussed as a 'thick' contribution API, and briefly as a Invoice API.)

NB: Although we have spec'd for the inclusion of pledge_payments below, we will be ignoring them in phase 1. The main reason is that pledge payments refer to contribution_ids and not line_items. Refactoring that will enable them to be supported as line_items in orders. At a UI level, this enables payments against pledges to be included in a combined payment for other outstanding items.

Get

Here is a sample PHP call to get an order:

civicrm_api3('order','get', array( 'sequential' => 1, 'contribution_id' => 11 ) ); 

The return value might be something along the following lines, basically the return value for a contribution get, with the return value from a get on its line_items included as an element:

{
 "is_error":0,
 "version":3,
 "count":2,
 "values":[{

 "contact_id":"43",

 "contact_type":"Individual",

 "contact_sub_type":"",

 "sort_name":"Roberts, Kiara",

 "display_name":"Dr. Kiara Roberts",

 "contribution_id":"11",

 "currency":"USD",

 "receive_date":"2009-07-01 12:55:41",

 "non_deductible_amount":"0.00",

 "total_amount":"300.00",

 "fee_amount":"",

 "net_amount":"",

 "trxn_id":"PL43II",

 "invoice_id":"",

 "cancel_date":"",

 "cancel_reason":"",

 "receipt_date":"",

 "thankyou_date":"",

 "contribution_source":"",

 "amount_level":"",

 "is_test":"0",

 "is_pay_later":"0",

 "contribution_status_id":"1",

 "check_number":"",

 "contribution_campaign_id":"",

 "financial_type_id":"1",

 "financial_type":"Donation",

 "instrument_id":"84",

 "payment_instrument":"Credit Card",

 "product_id":"",

 "product_name":"",

 "sku":"",

 "contribution_product_id":"",

 "product_option":"",

 "fulfilled_date":"",

 "contribution_start_date":"",

 "contribution_end_date":"",

 "contribution_recur_id":"",

 "financial_account_id":"1",

 "accounting_code":"4200",

 "contribution_note":"",

 "contribution_batch":"",

 "contribution_status":"Completed",

 "contribution_payment_instrument":"Credit Card",

 "contribution_check_number":"",

 "civicrm_value_donor_information_3_id":"",

 "custom_5":"",

 "custom_6":"",

 "id":"11",

 "contribution_type_id":"1"

"line_items":[{

 "id":"13",

 "entity_table":"civicrm_contribution",

 "entity_id":"11",

 "contribution_id":"11",

 "price_field_id":"1",

 "label":"Contribution Amount",

 "qty":"1",

 "unit_price":"200.00",

 "line_total":"200.00",

 "participant_count":"0",

 "price_field_value_id":"1",

 "financial_type_id":"1",

 "deductible_amount":"0.00"

 },

 {

 "id":"16",

 "entity_table":"civicrm_membership",

 "entity_id":"1",

 "contribution_id":"11",

 "price_field_id":"4",

 "label":"General",

 "qty":"1",

 "unit_price":"100.00",

 "line_total":"100.00",

 "price_field_value_id":"7",

 "financial_type_id":"2",

 "deductible_amount":"0.00"

 }]

    }]
}

Create

Here is a sample PHP call to create an order, with explanation of semantics below.

$result = civicrm_api3('order', 'create', array(
  'total_amount' => 50,  /* if not present, will be added based on sum of line_items. If present, must equal sum of line_items */
  'financial_type_id' => 4, /* if a line_item does not have an entry for financial_type_id, default to this one */
  'payment_instrument_id' => 'Check',
  'contact_id' => 7,
  'line_items' => array ( 
    array( /* line_item_with_param */
      'line_item' => array(
        'entity_table' => 'civicrm_participant',
        'entity_id' => NULL,
        ....  /* other values both required and optional to create this line item */
      ),
      'params' => array(...), /* will be used to create participant */
    ),
    array( /* line_item_with_param */
      'line_item' => array(
        'entity_table' => 'civicrm_participant',
        'entity_id' => NULL,
        .... /* other values both required and optional to create this line item */      ),
      'params' => array(...), /* will be used to create participant */
    ),
    array( /* line_item_with_param */
      'line_item' => array(
        'entity_table' => 'civicrm_membership',
        'entity_id' => NULL,
        .... /* other values both required and optional to create this line item */
      ),
      'params' => array(...), /* will be used to create membership */
    ),
    array(  /* line_item_with_param */
      'line_item' => array(
        'entity_table' => 'civicrm_contribution', /* this indicates the line item is just part of a contribution and has no related object */
        'entity_id' => NULL,
        .... /* other values both required and optional to create this line item */
      ),
      'params' => NULL, /* not needed */
    ),
    array(  /* line_item_with_param */
      'line_item' => array(
        'entity_table' => 'civicrm_contribution',
        'entity_id' => NULL,
        .... /* other values both required and optional to create this line item */
      ),
      'params' => array ( 'civicrm_pledge.id' => 2,

          ... ) , /* will be used to create payment against pledge */

    ),
  ),
);

['line_item_with_params'][n]['line_item']['entity_table'] is required and must be one of { 'civicrm_contribution', 'civicrm_membership', 'civicrm_participant', 'civicrm_pledge_payment' }.

If ['line_item_with_params'][n]['line_item']['entity_table'] == 'civicrm_contribution' then ['line_item_with_params'][n]['params'] must be null or not present.

If ['line_item_with_params'][n]['line_item']['entity_id'] is null or not present, that indicates the API should create the line_item and its related object if any (membership, participant, pledge_payment). On create, the entity_id is required to be null or not present (at least for phase 1). On update and delete, it refers to the object to be updated or deleted. On read, the value is ignored.

Update 

  1. This will be a very thin wrapper for the line_item id, with an update of the contribution.total_amount (and net_amount, fee_amount, etc) as appropriate.
  2. A non-null id or contribution_id field will be required, which will need to match an existing contribution_id. 
  3. An id or line_item_id field will be allowed within each line_item array. If both exist they must have the same value. Explanation below assumes that id is set to value of line_item_id.
    1. if a line_item has no id field or one with a null value, then it is interpreted to mean this line_item is being inserted. Associated $params will be used to create the relevant associated object.
    2. if a line_item has an id field it must exist and be linked in db to the contribution. This is interpreted as an update of the line_item. All fields provided are treated as params for update on line_item. If the line_item.entity_table <> 'civicrm_contribution' and the associated $params is not null then it will be checked to ensure that it refers to the correct related entity for the line_item in the db, and if so an update will be done against that object using the params. It is valid to not pass $params or to pass a null value, which is interpreted to mean no update against the associated object is desired.
    3. if a line_item in the db for the contribution is not passed as an argument to the update, this is interpreted as a 'deletion' of the line_item. (For clarity, this means a difference transaction to reverse the original bookkeeping entry will be created.) Currently, deleting a contribution that paid for a membership or participant record does not delete the associated membership or participant, but does remove the pledge_payment. Under the hood, the member_payment and participant_payment are also deleted. The order API will interpret deletion of line_items in the same way.
  4. If financial_type_id is changed, then the old line_item is reversed (using its qty and unit_price, etc.) and a new one is inserted with same values for qty, unit_price, etc. A second transaction will do the changes to other fields as if there were no change in financial_type.
  5. line_total is set to qty*unit_price
  6. Validation ensures that deductible_amount<=line_total and tax_amount<=line_total, qty*unit_price==line_total if line_total is present, and that no changes are being requested to the following fields:
    1. entity_table, entity_id, contribution_id, price_field_id, price_field_value_id, unit_price
  7. Validation ensures that tax_amount if provided is what tax calculations for the financial_type_id result in. 
  8. If not provided, tax_amount is set to result of tax calculations for financial_type_id for the line_total. NB: Pradeep, please help fill out spec on how taxes are handled, as these create additional financial_items.
  9. The update overwrites fields with no further changes for the following fields
    1. label, deductible_amount
  10. The update handles changes to participant_count in same way as form submit
  11. If there is a change to qty (and thus line_total), then a line_item reversal is done (this is separate method on API), and a new line item is inserted, with taxes being redone.
  12. Leave as a TODO in code: check if the financial_items for taxes would be changed in amount or financial_account_id compared to current tax financial items on the line item (this involves calculating current balance on each tax account for line_item).
  13. For reversal of line_item, we need to reverse the payments against it (based on sum(entity_financial_trxn.total)) as well as the item itself. 

Delete

We will include this as part of phase I. Use cases that make sense are: 1) to delete test transactions, and perhaps something to flush / rollback imports that didn't work including adjusting membership issues. 

A non-null id or contribution_id field will be required, which will need to match an existing contribution_id. It should not include the Invoice Prefix available in 4.6+ at civicrm/admin/setting/preferences/contribute?reset=1.

The implementation will be to call 

The line items of the contribution will be deleted, along with associated membership_payment, participant_payment, and pledge_payment records, but not associated memberships or participants. 

To repeat what should be a familiar refrain: It is inappropriate to delete or allow deletion of bookkeeping entries. This should only be allowed for test contributions. In normal operation the delete contribution permission should not be enabled for any user.

The proper way to 'delete' contributions should be to 'cancel' them.


Cancel

This action will result in the order being cancelled and all of the included objects such as membership and participant creation and updates. We will not be supporting a rollback of any update of information on a membership renewal in Phase 1 beyond the status and date fields (we don't currently keep the information that is overwritten during an update).

Permissions

No new permissions will be created to support this API. The permissions required to create and update contributions, memberships, participants and pledge_payments will determine if the order CRUD can complete all of its work. Transactions will be used to ensure that if any of the required accesses fail, then the whole access for the CRUD fails.