diff --git a/Tests/Recurly/Account_Test.php b/Tests/Recurly/Account_Test.php index d14edc47..d8f30e23 100644 --- a/Tests/Recurly/Account_Test.php +++ b/Tests/Recurly/Account_Test.php @@ -33,6 +33,7 @@ public function testGetAccount() { $this->assertEquals($account->cc_emails, 'cheryl.hines@example.com,richard.lewis@example.com'); $this->assertEquals($account->has_paused_subscription, false); $this->assertEquals($account->preferred_locale, 'en-US'); + $this->assertEquals($account->dunning_campaign_id, '1234abcd'); $this->assertInstanceOf('Recurly_CustomFieldList', $account->custom_fields); $this->assertCount(2, $account->custom_fields); @@ -103,6 +104,7 @@ public function testXml() { $account->exemption_certificate = 'Some Certificate'; $account->entity_use_code = 'I'; $account->preferred_locale = 'en-US'; + $account->dunning_campaign_id = '1234abcd'; $account_acquisition = new Recurly_AccountAcquisition(); $account_acquisition->cost_in_cents = 599; @@ -145,7 +147,7 @@ public function testXml() { $account->custom_fields[] = new Recurly_CustomField("serial_number", "4567-8900-1234"); $this->assertEquals( - "\nact123Verena
123 Main St.
falseI123 Main St.San FranciscoCA94110US555-555-5555verena@example.comWorkVerenaExampleRecurly Inc.123 Dolores St.San FranciscoCA94110US555-555-5555verena@example.comHomeVerenaExampleen-USserial_number4567-8900-1234599USDmarketing_contentpickle sticks blog postmailchimp67a904de95.0914d8f4b4Some Certificate
\n", + "\nact123Verena
123 Main St.
falseI123 Main St.San FranciscoCA94110US555-555-5555verena@example.comWorkVerenaExampleRecurly Inc.123 Dolores St.San FranciscoCA94110US555-555-5555verena@example.comHomeVerenaExampleen-USserial_number4567-8900-1234599USDmarketing_contentpickle sticks blog postmailchimp67a904de95.0914d8f4b4Some Certificate1234abcd
\n", $account->xml() ); } diff --git a/Tests/Recurly/DunningCampaign_List_Test.php b/Tests/Recurly/DunningCampaign_List_Test.php new file mode 100644 index 00000000..14eac02d --- /dev/null +++ b/Tests/Recurly/DunningCampaign_List_Test.php @@ -0,0 +1,16 @@ +client); + + $this->assertInstanceOf('Recurly_DunningCampaignList', $dunning_campaigns); + } +} diff --git a/Tests/Recurly/DunningCampaign_Test.php b/Tests/Recurly/DunningCampaign_Test.php new file mode 100644 index 00000000..9b1bfb01 --- /dev/null +++ b/Tests/Recurly/DunningCampaign_Test.php @@ -0,0 +1,49 @@ +client); + + $this->assertInstanceOf('Recurly_DunningCampaign', $dunning_campaign); + $this->assertEquals(3, $dunning_campaign->dunning_cycles[0]->intervals[0]->days); + } + + public function testBulkUpdateSuccess() { + $this->client->addResponse('PUT', '/dunning_campaigns/1234abcd/bulk_update', 'dunning_campaigns/update-204.xml'); + + $dunning_campaign = Recurly_DunningCampaign::get('1234abcd', $this->client); + + $plan_1 = new Recurly_Plan(); + $plan_1->plan_code = 'platinum'; + $plan_1->name = 'Platinum & Gold Plan'; + $plan_1->unit_amount_in_cents->addCurrency('USD', 1500); + $plan_1->unit_amount_in_cents->addCurrency('EUR', 1200); + $plan_1->setup_fee_in_cents->addCurrency('EUR', 500); + $plan_1->trial_requires_billing_info = false; + $plan_1->total_billing_cycles = 6; + $plan_1->auto_renew = false; + + $bulk_update_response = $dunning_campaign->bulkUpdate([$plan_1->plan_code]); + + $this->assertNull($bulk_update_response); + } + + public function testBulkUpdateFailure() { + $this->client->addResponse('PUT', '/dunning_campaigns/1234abcd/bulk_update', 'dunning_campaigns/update-404.xml'); + + $dunning_campaign = Recurly_DunningCampaign::get('1234abcd', $this->client); + + try { + $bulk_update_response = $dunning_campaign->bulkUpdate(['foo']); + } catch (Exception $e) { + $this->assertInstanceOf('Recurly_NotFoundError', $e); + } + } +} diff --git a/Tests/Recurly/Invoice_Test.php b/Tests/Recurly/Invoice_Test.php index cdd8e2e7..ece4429e 100644 --- a/Tests/Recurly/Invoice_Test.php +++ b/Tests/Recurly/Invoice_Test.php @@ -29,6 +29,7 @@ public function testGetInvoice() { $this->assertEquals($invoice->customer_notes, 'Some Customer Notes'); $this->assertEquals($invoice->vat_reverse_charge_notes, 'Some VAT Notes'); $this->assertEquals($invoice->invoice_number_prefix, ''); + $this->assertEquals($invoice->dunning_campaign_id, '1234abcd'); $this->assertEquals($invoice->invoiceNumberWithPrefix(), '1001'); } diff --git a/Tests/Recurly/Plan_Test.php b/Tests/Recurly/Plan_Test.php index 586ad073..3614b48a 100644 --- a/Tests/Recurly/Plan_Test.php +++ b/Tests/Recurly/Plan_Test.php @@ -42,6 +42,7 @@ public function testGetPlan() { $this->assertEquals(500, $plan->setup_fee_in_cents['USD']->amount_in_cents); $this->assertEquals(400, $plan->setup_fee_in_cents['EUR']->amount_in_cents); $this->assertTrue($plan->tax_exempt); + $this->assertEquals('1234abcd', $plan->dunning_campaign_id); } public function testDeletePlan() { @@ -60,9 +61,10 @@ public function testCreateXml() { $plan->trial_requires_billing_info = false; $plan->total_billing_cycles = 6; $plan->auto_renew = false; + $plan->dunning_campaign_id = '1234abcd'; $this->assertEquals( - "\nplatinumPlatinum & Gold Plan150012005006falsefalse\n", + "\nplatinumPlatinum & Gold Plan150012005006falsefalse1234abcd\n", $plan->xml() ); } @@ -78,9 +80,10 @@ public function testUpdateXml() { $plan->tax_exempt = false; $plan->trial_requires_billing_info = false; $plan->tax_code = 'fake-tax-code'; + $plan->dunning_campaign_id = '1234abcd'; $this->assertEquals( - "\nplatinumPlatinum Plan15001200500500falsefake-tax-codefalse\n", + "\nplatinumPlatinum Plan15001200500500falsefake-tax-codefalse1234abcd\n", $plan->xml() ); } diff --git a/Tests/Recurly/Purchase_Test.php b/Tests/Recurly/Purchase_Test.php index 3399d1d7..870b22f3 100644 --- a/Tests/Recurly/Purchase_Test.php +++ b/Tests/Recurly/Purchase_Test.php @@ -34,6 +34,7 @@ public function mockPurchase() { $purchase->account->address->country = "US"; $purchase->account->billing_info = new Recurly_BillingInfo(); $purchase->account->billing_info->token_id = '7z6furn4jvb9'; + $purchase->account->dunning_campaign_id = '1234abcd'; $shipping_address = new Recurly_ShippingAddress(); $shipping_address->first_name = 'Dolores'; @@ -62,7 +63,7 @@ public function testXml() { $purchase = $this->mockPurchase(); $this->assertEquals( - "\naba9209a-aa61-4790-8e61-0a2692435fee7z6furn4jvb9
123 Main St.San FranciscoCA94110US555-555-5555
USD10001at_invoiceabcd123automaticUSDCustomer NotesTerms and ConditionsVAT Reverse Charge Notes400 Dolores StSan FranciscoCA94110USHomeDoloresDu MondeaBcD1234moto
\n", + "\naba9209a-aa61-4790-8e61-0a2692435fee7z6furn4jvb9
123 Main St.San FranciscoCA94110US555-555-5555
1234abcd
USD10001at_invoiceabcd123automaticUSDCustomer NotesTerms and ConditionsVAT Reverse Charge Notes400 Dolores StSan FranciscoCA94110USHomeDoloresDu MondeaBcD1234moto
\n", $purchase->xml() ); } @@ -153,4 +154,3 @@ public function testTransactionError() { } } } - diff --git a/Tests/Recurly/Subscription_Test.php b/Tests/Recurly/Subscription_Test.php index d76021fb..c0cddb8b 100644 --- a/Tests/Recurly/Subscription_Test.php +++ b/Tests/Recurly/Subscription_Test.php @@ -83,6 +83,7 @@ public function testCreateSubscriptionXml() { $account->last_name = 'Example'; $account->email = 'verena@example.com'; $account->accept_language = 'en-US'; + $account->dunning_campaign_id = '1234abcd'; $billing_info = new Recurly_BillingInfo(); $billing_info->first_name = 'Verena'; @@ -116,7 +117,7 @@ public function testCreateSubscriptionXml() { $subscription->renewal_billing_cycles = 1; $this->assertEquals( - "\naccount_codeusernameVerenaExampleverena@example.comen-USVerenaExample192.168.0.14111-1111-1111-1111112015123gold1USDtrueSome Terms and ConditionsSome Customer Notes123 Main St.San FranciscoCA94110US555-555-5555verena@example.comWorkVerenaExampleRecurly Inc.serial_number4567-8900-1234false1\n", + "\naccount_codeusernameVerenaExampleverena@example.comen-USVerenaExample192.168.0.14111-1111-1111-11111120151231234abcdgold1USDtrueSome Terms and ConditionsSome Customer Notes123 Main St.San FranciscoCA94110US555-555-5555verena@example.comWorkVerenaExampleRecurly Inc.serial_number4567-8900-1234false1\n", $subscription->xml() ); } @@ -332,7 +333,7 @@ public function testPostponeSubscription() { $subscription = Recurly_Subscription::get('012345678901234567890123456789ab', $this->client); $subscription->postpone(date('c', strtotime('2019-12-31Z'))); - + $this->assertEquals(new DateTime('2019-12-31Z'), $subscription->current_period_ends_at); } @@ -355,10 +356,10 @@ public function testConvertTrialWith3DSToken() { public function testConvertTrialWithout3DSToken() { $this->client->addResponse('GET', '/subscriptions/012345678901234567890123456789ab', 'subscriptions/show-200-trial.xml'); $this->client->addResponse('PUT', 'https://api.recurly.com/v2/subscriptions/012345678901234567890123456789ab/convert_trial', 'subscriptions/convert-trial-200.xml'); - $subscription = Recurly_Subscription::get('012345678901234567890123456789ab', $this->client); + $subscription = Recurly_Subscription::get('012345678901234567890123456789ab', $this->client); $subscription->convertTrial(); $this->assertEquals($subscription->trial_ends_at, $subscription->current_period_started_at); } -} +} diff --git a/Tests/fixtures/accounts/show-200.xml b/Tests/fixtures/accounts/show-200.xml index c1afeda4..89ca735b 100644 --- a/Tests/fixtures/accounts/show-200.xml +++ b/Tests/fixtures/accounts/show-200.xml @@ -50,4 +50,5 @@ Content-Type: application/xml; charset=utf-8 false false true + 1234abcd diff --git a/Tests/fixtures/dunning_campaigns/index-200.xml b/Tests/fixtures/dunning_campaigns/index-200.xml new file mode 100644 index 00000000..a0207293 --- /dev/null +++ b/Tests/fixtures/dunning_campaigns/index-200.xml @@ -0,0 +1,161 @@ +HTTP/1.1 200 OK +Content-Type: application/xml; charset=utf-8 + + + + + pcnqbl7k1lq8 + newdefault + newdefault + + false + 2021-08-09T22:38:52Z + 2021-08-09T22:38:52Z + + + + automatic + false + 0 + false + + + 0 + payment_declined + + + 5 + paymentdeclined2 + + + 4 + paymentdeclined2 + + + 1 + paymentdeclined2 + + + true + true + 10 + 10 + 9 + 2021-08-31T15:56:03Z + 2021-08-31T15:56:03Z + + + manual + false + 0 + false + + + 0 + invoice_past_due + + + 3 + subscription_canceled_nonpayment + + + true + true + 3 + 3 + 8 + 2021-08-09T23:16:01Z + 2021-08-09T23:16:01Z + + + trial + true + 0 + false + + + 0 + post_trial_payment_declined + + + 3 + post_trial_payment_declined + + + 4 + post_trial_payment_declined + + + 3 + subscription_canceled_nonpayment + + + true + true + 10 + 10 + 1 + 2021-08-31T15:56:03Z + 2021-08-31T15:56:03Z + + + + + pbhylb5ud0wx + notdefaultthing + notdefaultthing + + false + 2021-08-04T02:10:24Z + 2021-08-04T02:10:24Z + + + + automatic + false + 0 + false + + + 0 + subscription_canceled_nonpayment + + + true + true + 0 + 0 + 3 + 2021-08-05T19:18:01Z + 2021-08-05T19:18:01Z + + + manual + false + 0 + false + + + 0 + invoice_past_due + + + 7 + invoice_past_due + + + 3 + subscription_canceled_nonpayment + + + true + true + 10 + 10 + 3 + 2021-08-05T19:18:01Z + 2021-08-05T19:18:01Z + + + + diff --git a/Tests/fixtures/dunning_campaigns/show-200.xml b/Tests/fixtures/dunning_campaigns/show-200.xml new file mode 100644 index 00000000..e0be9d88 --- /dev/null +++ b/Tests/fixtures/dunning_campaigns/show-200.xml @@ -0,0 +1,36 @@ +HTTP/1.1 200 OK +Content-Type: application/xml; charset=utf-8 + + + + 1234abcd + code + name + + true + 2017-02-17T15:38:53Z + 2020-02-17T15:38:53Z + + + + automatic + false + 3 + true + + + 3 + subscription_canceled_nonpayment + + + true + true + 0 + 3 + 9 + 2021-08-05T23:06:18Z + 2021-08-05T23:06:18Z + + + + diff --git a/Tests/fixtures/dunning_campaigns/update-204.xml b/Tests/fixtures/dunning_campaigns/update-204.xml new file mode 100644 index 00000000..0074ded3 --- /dev/null +++ b/Tests/fixtures/dunning_campaigns/update-204.xml @@ -0,0 +1,2 @@ +HTTP/1.1 204 No Content + diff --git a/Tests/fixtures/dunning_campaigns/update-404.xml b/Tests/fixtures/dunning_campaigns/update-404.xml new file mode 100644 index 00000000..024b7add --- /dev/null +++ b/Tests/fixtures/dunning_campaigns/update-404.xml @@ -0,0 +1,8 @@ +HTTP/1.1 404 Not Found +Content-Type: application/xml; charset=utf-8 + + + + not_found + Couldn't find plans with codes ["foo"] + diff --git a/Tests/fixtures/invoices/show-200.xml b/Tests/fixtures/invoices/show-200.xml index e5a5a7ad..8845e24c 100644 --- a/Tests/fixtures/invoices/show-200.xml +++ b/Tests/fixtures/invoices/show-200.xml @@ -28,6 +28,7 @@ Content-Type: application/xml; charset=utf-8 + 1234abcd diff --git a/Tests/fixtures/plans/show-200.xml b/Tests/fixtures/plans/show-200.xml index 6516c731..bfe7c62c 100644 --- a/Tests/fixtures/plans/show-200.xml +++ b/Tests/fixtures/plans/show-200.xml @@ -33,4 +33,5 @@ Content-Type: application/xml; charset=utf-8 500 400 + 1234abcd diff --git a/lib/recurly.php b/lib/recurly.php index 5366f874..9b2bd304 100644 --- a/lib/recurly.php +++ b/lib/recurly.php @@ -31,6 +31,10 @@ require_once(__DIR__ . '/recurly/credit_payment_list.php'); require_once(__DIR__ . '/recurly/custom_field.php'); require_once(__DIR__ . '/recurly/custom_field_list.php'); +require_once(__DIR__ . '/recurly/dunning_campaign.php'); +require_once(__DIR__ . '/recurly/dunning_campaign_list.php'); +require_once(__DIR__ . '/recurly/dunning_cycle.php'); +require_once(__DIR__ . '/recurly/dunning_interval.php'); require_once(__DIR__ . '/recurly/unique_coupon_code_list.php'); require_once(__DIR__ . '/recurly/export_date.php'); require_once(__DIR__ . '/recurly/export_date_list.php'); diff --git a/lib/recurly/account.php b/lib/recurly/account.php index 2a6c1933..649c7d38 100644 --- a/lib/recurly/account.php +++ b/lib/recurly/account.php @@ -35,6 +35,7 @@ * @property DateTime $closed_at For closed accounts, the date and time it was closed. * @property Recurly_AccountAcquisition $account_acquisition The nested account acquisition information: cost_in_cents, currency, channel, subchannel, campaign. * @property string $transaction_type Indicates type of resulting transaction. accepted_values: "moto". + * @property string $dunning_campaign_id Unique ID to identify the dunning campaign used when dunning the invoice. */ class Recurly_Account extends Recurly_Resource { @@ -98,7 +99,7 @@ public function createBillingInfo($billingInfo, $client = null) { } $billingInfo -> _save(Recurly_Client::POST, $this->uri() . '/billing_infos'); } - + public function updateBillingInfo($billingInfo, $client = null) { if ($client) { $billingInfo -> _client = $client; @@ -141,7 +142,7 @@ protected function getWriteableAttributes() { 'email', 'company_name', 'accept_language', 'billing_info', 'address', 'tax_exempt', 'entity_use_code', 'cc_emails', 'shipping_addresses', 'preferred_locale', 'custom_fields', 'account_acquisition', 'exemption_certificate', - 'parent_account_code', 'transaction_type' + 'parent_account_code', 'transaction_type', 'dunning_campaign_id' ); } protected function getRequiredAttributes() { diff --git a/lib/recurly/base.php b/lib/recurly/base.php index 38c27c78..736060ea 100644 --- a/lib/recurly/base.php +++ b/lib/recurly/base.php @@ -259,6 +259,10 @@ public function getLinks() { 'details' => 'array', 'discount_in_cents' => 'Recurly_CurrencyList', 'delivery' => 'Recurly_Delivery', + 'dunning_campaign' => 'Recurly_DunningCampaign', + 'dunning_campaigns' => 'Recurly_DunningCampaignList', + 'dunning_cycle' => 'Recurly_DunningCycle', + 'dunning_cycles' => 'array', 'error' => 'Recurly_FieldError', 'errors' => 'Recurly_ErrorList', 'export_date' => 'Recurly_ExportDate', @@ -269,6 +273,8 @@ public function getLinks() { 'gift_card' => 'Recurly_GiftCard', 'gift_cards' => 'Recurly_GiftCardList', 'gifter_account' => 'Recurly_Account', + 'interval' => 'Recurly_DunningInterval', + 'intervals' => 'array', 'invoice' => 'Recurly_Invoice', 'invoices' => 'Recurly_InvoiceList', 'invoice_collection' => 'Recurly_InvoiceCollection', diff --git a/lib/recurly/client.php b/lib/recurly/client.php index 196b3a9b..5546910a 100644 --- a/lib/recurly/client.php +++ b/lib/recurly/client.php @@ -82,6 +82,7 @@ class Recurly_Client const PATH_TRANSACTIONS = 'transactions'; const PATH_MEASURED_UNITS = 'measured_units'; const PATH_USAGE = 'usage'; + const PATH_DUNNING_CAMPAIGNS = 'dunning_campaigns'; /** * Create a new Recurly Client diff --git a/lib/recurly/dunning_campaign.php b/lib/recurly/dunning_campaign.php new file mode 100644 index 00000000..39755b99 --- /dev/null +++ b/lib/recurly/dunning_campaign.php @@ -0,0 +1,55 @@ +createDocument(); + + $root = $doc->appendChild($doc->createElement($this->getNodeName())); + $planCodesNode = $root->appendChild($doc->createElement('plan_codes')); + + foreach ($planCodes as $planCode) { + $planCodeNode = $planCodesNode->appendChild($doc->createElement('plan_code', $planCode)); + } + + $response = $this->_client->request(Recurly_Client::PUT, $this->uri() . '/bulk_update', $this->renderXML($doc)); + $response->assertValidResponse(); + + return null; + } + + protected static function uriForDunningCampaign($id) { + return self::_safeUri(Recurly_Client::PATH_DUNNING_CAMPAIGNS, $id); + } + + protected function uri() { + if (!empty($this->_href)) + return $this->getHref(); + else + return Recurly_DunningCampaign::uriForDunningCampaign($this->id); + } + + protected function getNodeName() { + return 'dunning_campaign'; + } + + protected function getWriteableAttributes() { + return array(); + } +} diff --git a/lib/recurly/dunning_campaign_list.php b/lib/recurly/dunning_campaign_list.php new file mode 100644 index 00000000..8aa4cc4f --- /dev/null +++ b/lib/recurly/dunning_campaign_list.php @@ -0,0 +1,13 @@ +isEmbedded($node, 'intervals')) { + $intervalNode = $node->appendChild($doc->createElement($this->getNodeName())); + parent::populateXmlDoc($doc, $intervalNode, $obj, $nested); + } else { + parent::populateXmlDoc($doc, $node, $obj, $nested); + } + } +} diff --git a/lib/recurly/invoice.php b/lib/recurly/invoice.php index 12d6d7c9..1573c82b 100644 --- a/lib/recurly/invoice.php +++ b/lib/recurly/invoice.php @@ -43,6 +43,7 @@ * @property string $all_transactions A link to all transactions on the invoice. Only present if there are more than 500 transactions * @property int $subtotal_before_discount_in_cents The total of all adjustments on the invoice before discounts or taxes are applied. * @property int $credit_customer_notes Allows merchant to set customer notes on a credit invoice. Will only be rejected if type is set to "charge", otherwise will be ignored if no credit invoice is created. + * @property string $dunning_campaign_id Unique ID to identify the dunning campaign used when dunning the invoice. */ class Recurly_Invoice extends Recurly_Resource { diff --git a/lib/recurly/plan.php b/lib/recurly/plan.php index 74a43133..5ca45ed8 100644 --- a/lib/recurly/plan.php +++ b/lib/recurly/plan.php @@ -33,6 +33,7 @@ * @property boolean $trial_requires_billing_info Setting to determine if subscriptions to this plan will always require billing info or will only require it when either not in a trial or when money is due, defaults to true. * @property boolean $auto_renew Determines whether subscriptions to this plan should auto-renew term at the end of the current term or expire. Defaults to true. * @property boolean $allow_any_item_on_subscriptions Used to determine whether items can be assigned as add-ons to individual subscriptions. If `true`, items can be assigned as add-ons to individual subscription add-ons. If `false`, only plan add-ons can be used. + * @property string $dunning_campaign_id Unique ID to identify the dunning campaign used when dunning the invoice. */ class Recurly_Plan extends Recurly_Resource { @@ -99,7 +100,8 @@ protected function getWriteableAttributes() { 'trial_interval_unit', 'unit_amount_in_cents', 'setup_fee_in_cents', 'total_billing_cycles', 'accounting_code', 'setup_fee_accounting_code', 'revenue_schedule_type', 'setup_fee_revenue_schedule_type', - 'tax_exempt', 'tax_code', 'trial_requires_billing_info', 'auto_renew', 'allow_any_item_on_subscriptions' + 'tax_exempt', 'tax_code', 'trial_requires_billing_info', 'auto_renew', 'allow_any_item_on_subscriptions', + 'dunning_campaign_id' ); } }