Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support open amount and line item refunds #133

Merged
merged 1 commit into from
Feb 6, 2015
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
# Recurly PHP Client Library CHANGELOG

## Unreleased

* Added adjustment refund support: `refund()` and `toRefundAttributes()` on `Recurly_Adjustment` [133](https://github.com/recurly/recurly-client-php/pull/133)
* Added invoice refund supprt: `refund()` and `refundAmount()` on `Recurly_Invoice` [133](https://github.com/recurly/recurly-client-php/pull/133)

## Version 2.4.0 (Feb 2nd, 2014)

* Force cURL to validate SSL certificates [#122](https://github.com/recurly/recurly-client-php/pull/122)
Expand Down
22 changes: 22 additions & 0 deletions Tests/Recurly/Adjustment_Test.php
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,28 @@ public function testDelete() {
$adjustment->delete();
}

public function testToRefundAttributes() {
$this->client->addResponse('GET', '/adjustments/abcdef1234567890', 'adjustments/show-200.xml');

$adjustment = Recurly_Adjustment::get('abcdef1234567890', $this->client);

$attributes = $adjustment->toRefundAttributes();
$this->assertEquals($attributes['uuid'], $adjustment->uuid);
$this->assertEquals($attributes['prorate'], false);
$this->assertEquals($attributes['quantity'], $adjustment->quantity);
}

public function testAdjustmentRefund() {
$this->client->addResponse('GET', '/adjustments/abcdef1234567890', 'adjustments/show-200.xml');
$this->client->addResponse('GET', 'https://api.recurly.com/v2/invoices/1234', 'invoices/show-200.xml');
$this->client->addResponse('POST', 'https://api.recurly.com/v2/invoices/1001/refund', 'invoices/refund-201.xml');

$adjustment = Recurly_Adjustment::get('abcdef1234567890', $this->client);

$refund_invoice = $adjustment->refund();
$this->assertEquals($refund_invoice->subtotal_in_cents, -1000);
}

public function testXml() {
$charge = new Recurly_Adjustment();
$charge->account_code = '1';
Expand Down
22 changes: 22 additions & 0 deletions Tests/Recurly/Invoice_Test.php
Original file line number Diff line number Diff line change
Expand Up @@ -121,4 +121,26 @@ public function testGetInvoicePdf() {
$result = Recurly_Invoice::getInvoicePdf('1001', 'en-GB', $this->client);
$this->assertEquals(array('/invoices/1001', 'en-GB'), $result);
}

public function testRefundAmount() {
$this->client->addResponse('POST', 'https://api.recurly.com/v2/invoices/1001/refund', 'invoices/refund-201.xml');
$invoice = Recurly_Invoice::get('1001', $this->client);

$refund_invoice = $invoice->refundAmount(1000);
$this->assertEquals($refund_invoice->subtotal_in_cents, -1000);
}

public function testRefund() {
$this->client->addResponse('POST', 'https://api.recurly.com/v2/invoices/1001/refund', 'invoices/refund-201.xml');
$invoice = Recurly_Invoice::get('1001', $this->client);
$line_items = $invoice->line_items;

$adjustment_map = function($line_item) {
return $line_item->toRefundAttributes();
};
$adjustments = array_map($adjustment_map, $line_items);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Having the caller have to do this transformation seems a little unnecessary. I can see where you'd want to specify values for quantity/prorate but I wonder if we can come up with something a little nicer.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the pattern we use for the other libs:

https://github.com/recurly/recurly-client-ruby/blob/master/spec/recurly/invoice_spec.rb#L54
https://github.com/recurly/recurly-client-python/blob/master/tests/test_resources.py#L573

I agree it's not ideal. What did you have in mind? Perhaps we have a method on the Recurly_Adjustment class? We can't just use the adjustment itself because it needs the prorated field and they may not want to refund the entire quantity.


$refund_invoice = $invoice->refund($adjustments);
$this->assertEquals($refund_invoice->subtotal_in_cents, -1000);
}
}
21 changes: 21 additions & 0 deletions Tests/fixtures/invoices/refund-201.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
HTTP/1.1 201 Created
Content-Type: application/xml; charset=utf-8
Location: https://api.recurly.com/v2/invoices/created-invoice

<invoice href="https://api.recurly.com/v2/invoices/1001">
<account href="https://api.recurly.com/v2/accounts/1"/>
<subscription href="https://api.recurly.com/v2/subscriptions/1234567890abcdef"/>
<uuid>012345678901234567890123456789aa</uuid>
<state>collected</state>
<invoice_number type="integer">abcdef1234567890</invoice_number>
<po_number></po_number>
<vat_number></vat_number>
<subtotal_in_cents type="integer">-1000</subtotal_in_cents>
<tax_in_cents type="integer">0</tax_in_cents>
<total_in_cents type="integer">2995</total_in_cents>
<currency>USD</currency>
<created_at type="datetime">2012-05-28T17:44:13Z</created_at>
<closed_at type="datetime">2012-05-28T17:44:13Z</closed_at>
<net_terms type="integer">0</net_terms>
<collection_method>automatic</collection_method>
</invoice>
31 changes: 31 additions & 0 deletions lib/recurly/adjustment.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,37 @@ public function delete() {
return Recurly_Base::_delete($this->getHref(), $this->_client);
}

/**
* Allows you to refund this particular item if it's a part of
* an invoice. It does this by calling the invoice's refund()
* Only 'invoiced' adjustments can be refunded.
*
* @param Integer the quantity you wish to refund, defaults to refunding the entire quantity
* @param Boolean indicates whether you want this adjustment refund prorated
* @return Recurly_Invoice the new refund invoice
* @throws Recurly_Error if the adjustment cannot be refunded.
*/
public function refund($quantity = null, $prorate = false) {
if ($this->state == 'pending') {
throw new Recurly_Error("Only invoiced adjustments can be refunded");
}
$invoice = $this->invoice->get();
return $invoice->refund($this->toRefundAttributes($quantity, $prorate));
}

/**
* Converts this adjustment into the attributes needed for a refund.
*
* @param Integer the quantity you wish to refund, defaults to refunding the entire quantity
* @param Boolean indicates whether you want this adjustment refund prorated
* @return Array an array of refund attributes to be fed into invoice->refund()
*/
public function toRefundAttributes($quantity = null, $prorate = false) {
if (is_null($quantity)) $quantity = $this->quantity;

return array('uuid' => $this->uuid, 'quantity' => $quantity, 'prorate' => $prorate);
}

protected function createUriForAccount() {
if (empty($this->account_code))
throw new Recurly_Error("'account_code' is not specified");
Expand Down
41 changes: 41 additions & 0 deletions lib/recurly/invoice.php
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,47 @@ public function invoiceNumberWithPrefix() {
return $this->invoice_number_prefix . $this->invoice_number;
}

/**
* Refunds an open amount from the invoice and returns a new refund invoice
* @param Integer amount in cents to refund from this invoice
* @return Recurly_Invoice a new refund invoice
*/
public function refundAmount($amount_in_cents) {
$doc = $this->createDocument();

$root = $doc->appendChild($doc->createElement($this->getNodeName()));
$root->appendChild($doc->createElement('amount_in_cents', $amount_in_cents));

return $this->createRefundInvoice($this->renderXML($doc));
}

/**
* Refunds given line items from an invoice and returns new refund invoice
* @param Array refund attributes or Array of refund attributes to refund (see 'REFUND ATTRIBUTES' in docs or Recurly_Adjustment#toRefundAttributes)
* @return Recurly_Invoice a new refund invoice
*/
public function refund($line_items) {
if (isset($line_items['uuid'])) { $line_items = array($line_items); }

$doc = $this->createDocument();

$root = $doc->appendChild($doc->createElement($this->getNodeName()));
$line_items_node = $root->appendChild($doc->createElement('line_items'));

foreach ($line_items as $line_item) {
$adjustment_node = $line_items_node->appendChild($doc->createElement('adjustment'));
$adjustment_node->appendChild($doc->createElement('uuid', $line_item['uuid']));
$adjustment_node->appendChild($doc->createElement('quantity', $line_item['quantity']));
$adjustment_node->appendChild($doc->createElement('prorate', $line_item['prorate'] ? 'true' : 'false'));
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any way we can provide defaults for the quantity and proration?

}

return $this->createRefundInvoice($this->renderXML($doc));
}

protected function createRefundInvoice($xml_string) {
return self::_post($this->uri() . '/refund', $xml_string, $this->_client);
}

protected function getNodeName() {
return 'invoice';
}
Expand Down
10 changes: 9 additions & 1 deletion lib/recurly/resource.php
Original file line number Diff line number Diff line change
Expand Up @@ -91,11 +91,19 @@ protected function _save($method, $uri)

public function xml()
{
$doc = new DOMDocument("1.0");
$doc = $this->createDocument();
$root = $doc->appendChild($doc->createElement($this->getNodeName()));
$this->populateXmlDoc($doc, $root, $this);
// To be able to consistently run tests across different XML libraries,
// favor `<foo></foo>` over `<foo/>`.
return $this->renderXML($doc);
}

public function createDocument() {
return new DOMDocument("1.0");
}

public function renderXML($doc) {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like this whole refactoring.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

return $doc->saveXML(null, LIBXML_NOEMPTYTAG);
}

Expand Down