Aller directement à la fin des métadonnées
Aller au début des métadonnées

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 

  • Eileen is thinking maybe we should have specific actions like order.add_line_item, order.remove_line_item, order.update_line_item. In particular less keen on (c) below. Also what happens if some payment has been made?
  • Joe responds: I was thinking about providing both current-style API v3 and new style one like you suggest. With regard to c) when a full payment has been made, the order will have end up overpaid when an item is removed. Calling code will need to worry about creating a refund.
  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.

Étiquette
  • Aucun
  1. Apr 02, 2015

    Get

     

    I think the invoice.get is pretty similar to the api I use (account_invoice.getderived )

    https://github.com/eileenmcnaughton/nz.co.fuzion.accountsync/blob/master/api/v3/AccountInvoice.php#L76

     

    Create

    With create I think that since we are creating a new api we can just refer to 'line_items' rather than 'line_items_with_params'' (yay).

     

    I believe (strongly) that status_id should NOT be a possible param for create - it should always be pending - with separate actions to change that - like add_payment.

     

    Note that line_items params might look like

    'participant' => array(

      'event_id' => 8,

      'role_id' => 3,

    ),

    I would say that neither 'fee_amount' or 'status_id' should be options though.

     

    The challenge is a $0 participant record - I would probably still advocate create as pending & then complete. However, the complete might be triggered at the end of the create when it realises that paid_balance = required_balance = 0.

     

    Update

    Here are the sorts of updates I can thing of

    • Add line item
    • Remove line item
    • Alter line item without changing total
    • Alter line item with changing total
    • Cancel invoice
    • Add payment to invoice (leading to other updates depending how complete)
    • Add refund to invoice
    • Set non-fully paid invoice to paid - I believe this will result in a line being added to reflect the write-off

    In general my preference is not to treat these as a simple CRUD and to not have an update but instead to have specific actions for them. I believe most of them require fairly different processing & update gets too complicated quickly. The reason for this pseudo-api is to encapsulate business logic and I think it warrants a departure from crud. (I have been referring to the create action as 'prepare' to get away from the CRUD terms - but am not hard-core on that).

     

    Delete

    I have doubts about this one too. We DO have a contribution.delete which probably does the same thing. We could also add an api action like flush_test, of even make 'delete_test_entries' a param on system.flush api but I feel invoice.delete is possibly a little misleading.

     

     

  2. Apr 02, 2015

    One more tangental thing. In general I believe that event_id should be a field on the price_field_value - at the moment it seems to me there is a block to having events in the membership price set & it seems to me the reason is because 'which event' is defined by the join to the event page and reviewing that logic could really improve our code flexibility

  3. Apr 02, 2015

    Tim Otten dit :

    A couple preliminary questions:

    • At what point in the process is this API/virtual-entity supposed to come into play? The term "invoice" for me suggests a document that's generated after the general terms of the transaction have been established – e.g. after the shopping cart is submitted, the transaction moves into collections phase, and an invoice is generated. However, the substance of the API seems to anticipate adding/removing items and adjusting prices, which makes me think that the API/virtual-entity is actually intended to capture the entire lifecycle (e.g. beginning as a user populates a shopping cart and ending when payment is confirmed or perhaps refunded).
    • I don't know the contribution/financial tables that well, so I'm not sure if I'm reading "line_items_with_params" correctly. At a high-level, there seems to be some appealing/intuitive logic in making a 1-1 relation between a standardized financial record (civicrm_line_item – that looks the same for any kind of purchase) and an operational record about the specific item (civicrm_participant, civicrm_membership, etc). So that's good. But I get confused because civicrm_line_item has a "qty" field. If each civicrm_line_item has exactly one related operational record, wouldn't the qty be fixed at "1" (never more, never less)?
    • FWIW, if I didn't know anything about APIv3 or Civi's code, and if you asked me to write a simple/brainless API for building and modifying purchases in Civi's domain, it would probably look more like https://gist.github.com/totten/831da81e5d02918bcb9f
    1. Apr 02, 2015

      JoeMurray dit :

      Yes, the API is intended to serve the whole life-cycle from initial order, changes to what is being purchased/donated, payment(s), refunds, receipts, etc. Full-blown accounting systems have notions like sales order, purchase order, invoice, payments, credit notes... (See http://en.wikipedia.org/wiki/Sales_order,  http://en.wikipedia.org/wiki/Invoice, http://smallbusiness.chron.com/sales-order-vs-sales-invoice-20610.htmlhttp://smallbusiness.chron.com/work-order-vs-invoice-48573.html). Would order be the best term for this virtual object?

      The qty field is a bit of a work-around as I understand it. In contributions the price is set to 1.00 and the qty is the number of dollars to be given. For memberships, it is used to indicate the number of terms, I believe. For events, it is used to indicate things like the number of tables being ordered, with the participant count being the number of individual tickets. I think we should therefore define the $params to be an array to handle occasions when multiple registrants are being created at the same time for the same type of role. Will this be a hassle for memberships? Probably not much. I suppose it might be convenient for indicating multiple pledge_payments at once in some contexts.

      Very different kind of signature, but quite intuitive. I was hoping to leverage the existing API code as much as possible. Hmm.

  4. Apr 03, 2015

    So, as far as I'm concerned CREATE should only be the initial create of an intent. I strongly believe that by trying to determine whether the outcome is that it will be paid now, later or someday when creating is to blame for much complexity.

    If the word invoice is already causing confusion maybe we should switch to 'order' at this point.

    IMHO the other actions reflect adjustments. Adjustment actions might be initiated by
    - someone phones up and says ' I've been dumped for an under-age marsupial and need to cancel one of my event bookings'
    - the payment processor replying 'success'
    - the payment processor replying 'failure'
    - a cheque for some or all of the amount arriving
    - the cheque later bouncing


    In terms of create/ signature / line items - I've been coming at this from a slighty different angle in my head. I've been seeing the price sets as the starting point and thinking that what comes in from the price set should be augmented by the absolutely minimum amount of information to process an order. Part of the reason for this is that I see using a consistent api function from the various payment forms as the most pressing requirement.

    ie. if the form is likely to see submitted params

    array(
    contact_id => x,
    price_5 => 1,
    price_6 => array(7, 8),
    is_send_email => 0,
    );

    I'm currently looking at the backoffice participant form which uses over 1000 lines in it's post process. Event is not the best example (because I have tangental thoughts) - but say this was a membership price set - the above should be almost all the information to create the contribution, line items (correctly coded) and to create or renew one or more memberships (depending on the price set config).

    The price_5 => array() or 1 is pretty unintuitive so although I'd like an api to accept that 'as is' I'm not sure it's the full need. But, I believe that we should be able to see a post process that looks like this

    postProcess();
    $params = submitted stuff;
    // save custom fields & create contact
    civicrm_api3('order', 'create', $params);
    $paymentOutcome = $paymentProcessor->doPayment();
    switch ($paymentOutcome) {
    case 'super duper fantastic success':
    civicrm_api3('order', 'complete');
    case 'abject_failure':
    civicrm_api3('order', 'complete');
    }

    Now writing the above it is tempting of course to try to go for a more OO look. But, I'm thinking that we are working towards being able to 'lift' all this functionality & put in into a new form and I'm effectively imagining the existing 'event' & 'contribution' forms becoming obsolete - but the price-set driving the actions that happen off the form.

    I've recently looked at webform and realise that is perhaps not the direction you are envisaging Tim (& the example above perhaps illustrates that). (& at the moment I rejected using webform for commerce because there were accounting gaps)

  5. Apr 07, 2015

    As I have been doing some side-work on being able to submit a price_set I've decided I should add that as a price_set.submit api. Mostly I want to ensure that there is a tested-locked in path for this (because otherwise all the functions that are involved could easily be changed) - but I think this takes us along the path towards
    1) using a consistent function from the form layer
    2) more unit tests
    3) being able to remove the limit on adding memberships to even price sets - I would argue the price set is all the information required to add a membership to an event form.

    1. Apr 07, 2015

      JoeMurray dit :

      I don't mind seeing something like a price_set.submit exposed as an API- actually I'd love it -but it is a higher level API that is more at the presentation / form layer, in the buildForm function. The order and payment API would sit below that one, and be called after it in postProcess. From my perspective there is no requirement that line item values be derived from Price Sets, since we want to allow recording of purchases from outside of CiviCRM of items that may only exist in Drupal Commerce or WordPress wooCommerce.

      Sorry I didn't respond directly to your request to include a price set reference in the order API but as you have found that's a different layer of functionality.


Creative Commons License
Except where otherwise noted, content on this site is licensed under a Creative Commons Attribution-Share Alike 3.0 United States Licence.