001/*
002 * Copyright 2010-2014 Ning, Inc.
003 * Copyright 2014-2015 The Billing Project, LLC
004 *
005 * The Billing Project licenses this file to you under the Apache License, version 2.0
006 * (the "License"); you may not use this file except in compliance with the
007 * License.  You may obtain a copy of the License at:
008 *
009 *    http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
013 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
014 * License for the specific language governing permissions and limitations
015 * under the License.
016 */
017
018package com.ning.billing.recurly;
019
020import com.ning.billing.recurly.model.Account;
021import com.ning.billing.recurly.model.AccountBalance;
022import com.ning.billing.recurly.model.AccountNotes;
023import com.ning.billing.recurly.model.Accounts;
024import com.ning.billing.recurly.model.AddOn;
025import com.ning.billing.recurly.model.AddOns;
026import com.ning.billing.recurly.model.Adjustment;
027import com.ning.billing.recurly.model.AdjustmentRefund;
028import com.ning.billing.recurly.model.Adjustments;
029import com.ning.billing.recurly.model.BillingInfo;
030import com.ning.billing.recurly.model.BillingInfos;
031import com.ning.billing.recurly.model.BillingInfoVerification;
032import com.ning.billing.recurly.model.Coupon;
033import com.ning.billing.recurly.model.Coupons;
034import com.ning.billing.recurly.model.CreditPayments;
035import com.ning.billing.recurly.model.Errors;
036import com.ning.billing.recurly.model.GiftCard;
037import com.ning.billing.recurly.model.GiftCards;
038import com.ning.billing.recurly.model.Invoice;
039import com.ning.billing.recurly.model.InvoiceCollection;
040import com.ning.billing.recurly.model.InvoiceRefund;
041import com.ning.billing.recurly.model.InvoiceState;
042import com.ning.billing.recurly.model.Invoices;
043import com.ning.billing.recurly.model.Item;
044import com.ning.billing.recurly.model.Items;
045import com.ning.billing.recurly.model.Plan;
046import com.ning.billing.recurly.model.Plans;
047import com.ning.billing.recurly.model.Purchase;
048import com.ning.billing.recurly.model.RecurlyAPIError;
049import com.ning.billing.recurly.model.RecurlyObject;
050import com.ning.billing.recurly.model.RecurlyObjects;
051import com.ning.billing.recurly.model.Redemption;
052import com.ning.billing.recurly.model.Redemptions;
053import com.ning.billing.recurly.model.RefundMethod;
054import com.ning.billing.recurly.model.RefundOption;
055import com.ning.billing.recurly.model.ShippingAddress;
056import com.ning.billing.recurly.model.ShippingAddresses;
057import com.ning.billing.recurly.model.Subscription;
058import com.ning.billing.recurly.model.SubscriptionState;
059import com.ning.billing.recurly.model.SubscriptionUpdate;
060import com.ning.billing.recurly.model.SubscriptionNotes;
061import com.ning.billing.recurly.model.Subscriptions;
062import com.ning.billing.recurly.model.Transaction;
063import com.ning.billing.recurly.model.TransactionState;
064import com.ning.billing.recurly.model.TransactionType;
065import com.ning.billing.recurly.model.Transactions;
066import com.ning.billing.recurly.model.Usage;
067import com.ning.billing.recurly.model.Usages;
068import com.ning.billing.recurly.model.MeasuredUnit;
069import com.ning.billing.recurly.model.MeasuredUnits;
070import com.ning.billing.recurly.model.AccountAcquisition;
071import com.ning.billing.recurly.model.ShippingMethod;
072import com.ning.billing.recurly.model.ShippingMethods;
073import com.google.common.annotations.VisibleForTesting;
074import com.google.common.base.Charsets;
075import com.google.common.base.MoreObjects;
076import com.google.common.base.StandardSystemProperty;
077import com.google.common.collect.ImmutableSet;
078import com.google.common.io.BaseEncoding;
079import com.google.common.net.HttpHeaders;
080import com.ning.billing.recurly.util.http.SslUtils;
081
082import org.apache.commons.codec.net.URLCodec;
083import org.apache.http.Header;
084import org.apache.http.HttpEntity;
085import org.apache.http.NoHttpResponseException;
086import org.apache.http.ParseException;
087import org.apache.http.client.config.RequestConfig;
088import org.apache.http.client.methods.CloseableHttpResponse;
089import org.apache.http.client.methods.HttpDelete;
090import org.apache.http.client.methods.HttpGet;
091import org.apache.http.client.methods.HttpHead;
092import org.apache.http.client.methods.HttpPost;
093import org.apache.http.client.methods.HttpPut;
094import org.apache.http.client.methods.HttpRequestBase;
095import org.apache.http.conn.ConnectTimeoutException;
096import org.apache.http.entity.ContentType;
097import org.apache.http.entity.StringEntity;
098import org.apache.http.impl.client.CloseableHttpClient;
099import org.apache.http.impl.client.HttpClientBuilder;
100import org.apache.http.impl.client.HttpClients;
101import org.apache.http.message.HeaderGroup;
102import org.apache.http.util.EntityUtils;
103import org.joda.time.DateTime;
104import org.slf4j.Logger;
105import org.slf4j.LoggerFactory;
106
107import javax.annotation.Nullable;
108import javax.net.ssl.SSLException;
109
110import java.io.ByteArrayInputStream;
111import java.io.IOException;
112import java.io.InputStream;
113import java.io.InputStreamReader;
114import java.io.Reader;
115import java.math.BigDecimal;
116import java.net.ConnectException;
117import java.net.URI;
118import java.net.URL;
119import java.net.URLEncoder;
120import java.security.KeyManagementException;
121import java.security.NoSuchAlgorithmException;
122import java.util.Properties;
123import java.util.Set;
124import java.util.regex.Matcher;
125import java.util.regex.Pattern;
126import java.util.BitSet;
127import java.util.List;
128import java.util.Locale;
129
130public class RecurlyClient {
131
132    private static final Logger log = LoggerFactory.getLogger(RecurlyClient.class);
133
134    public static final String RECURLY_DEBUG_KEY = "recurly.debug";
135    public static final String RECURLY_API_VERSION = "2.29";
136
137    private static final String X_RATELIMIT_REMAINING_HEADER_NAME = "X-RateLimit-Remaining";
138    private static final String X_RECORDS_HEADER_NAME = "X-Records";
139    private static final String LINK_HEADER_NAME = "Link";
140
141    private static final String GIT_PROPERTIES_FILE = "com/ning/billing/recurly/git.properties";
142    @VisibleForTesting
143    static final String GIT_COMMIT_ID_DESCRIBE_SHORT = "git.commit.id.describe-short";
144    private static final Pattern TAG_FROM_GIT_DESCRIBE_PATTERN = Pattern.compile("recurly-java-library-([0-9]*\\.[0-9]*\\.[0-9]*)(-[0-9]*)?");
145
146    public static final String FETCH_RESOURCE = "/recurly_js/result";
147
148    private static final Set<String> validHosts = ImmutableSet.of("recurly.com");
149
150    /**
151     * RFC-3986 unreserved characters used for standard URL encoding.<br>
152     * <a href="https://tools.ietf.org/html/rfc3986#section-2.3">Source</a>
153     */
154    private static final BitSet RFC_3986_SAFE_CHARS;
155    static {
156        RFC_3986_SAFE_CHARS = new BitSet(256);
157        RFC_3986_SAFE_CHARS.set('a', 'z' + 1);
158        RFC_3986_SAFE_CHARS.set('A', 'Z' + 1);
159        RFC_3986_SAFE_CHARS.set('0', '9' + 1);
160        RFC_3986_SAFE_CHARS.set('-');
161        RFC_3986_SAFE_CHARS.set('_');
162        RFC_3986_SAFE_CHARS.set('.');
163        RFC_3986_SAFE_CHARS.set('~');
164    }
165
166    /**
167     * Checks a system property to see if debugging output is
168     * required. Used internally by the client to decide whether to
169     * generate debug output
170     */
171    private static boolean debug() {
172        return Boolean.getBoolean(RECURLY_DEBUG_KEY);
173    }
174
175    /**
176     * Warns the user about logging PII in production environments
177     */
178    private static void loggerWarning() {
179        if (debug())
180        {
181            log.warn("[WARNING] Logger enabled. The logger has the potential to leak " +
182            "PII and should never be used in production environments.");
183        }
184    }
185
186    private final String userAgent;
187
188    private final String key;
189    private final String baseUrl;
190    private CloseableHttpClient client;
191
192    // Allows error messages to be returned in a specified language
193    private String acceptLanguage = "en-US";
194
195    // Stores the number of requests remaining before rate limiting takes effect
196    private int rateLimitRemaining;
197
198    public RecurlyClient(final String apiKey) {
199        this(apiKey, "api");
200        loggerWarning();
201    }
202
203    public RecurlyClient(final String apiKey, final String subDomain) {
204        this(apiKey, subDomain + ".recurly.com", 443, "v2");
205        loggerWarning();
206    }
207
208    public RecurlyClient(final String apiKey, final String host, final int port, final String version) {
209        this(apiKey, "https", host, port, version);
210        loggerWarning();
211    }
212
213    public RecurlyClient(final String apiKey, final String scheme, final String host, final int port, final String version) {
214        this.key = BaseEncoding.base64().encode(apiKey.getBytes(Charsets.UTF_8));
215        this.baseUrl = String.format(Locale.ROOT, "%s://%s:%d/%s", scheme, host, port, version);
216        this.userAgent = UserAgentHolder.userAgent;
217        this.rateLimitRemaining = -1;
218        loggerWarning();
219    }
220
221    /**
222     * Open the underlying http client
223     */
224    public synchronized void open() throws NoSuchAlgorithmException, KeyManagementException {
225        client = createHttpClient();
226    }
227
228    /**
229     * Close the underlying http client
230     */
231    public synchronized void close() {
232        if (client != null) {
233            try {
234                client.close();
235            } catch (IOException e) {
236                throw new RuntimeException(e);
237            }
238        }
239    }
240
241    /**
242     * Set the Accept-Language header
243     * <p>
244     * Sets the Accept-Language header for all requests made by this client. Note: this is not thread-safe!
245     * See https://github.com/killbilling/recurly-java-library/pull/298 for more details about thread safety.
246     *
247     * @param language The language to set in the header. E.g., "en-US"
248     */
249    public void setAcceptLanguage(String language) {
250        this.acceptLanguage = language;
251    }
252
253    /**
254     * Returns the number of requests remaining until requests will be denied by rate limiting.
255     * @return Number of requests remaining. Value is valid (> -1) after a successful API call.
256     */
257    public int getRateLimitRemaining() {
258        return rateLimitRemaining;
259    }
260
261    /**
262     * Create Account
263     * <p>
264     * Creates a new account. You may optionally include billing information.
265     *
266     * @param account account object
267     * @return the newly created account object on success, null otherwise
268     */
269    public Account createAccount(final Account account) {
270        return doPOST(Account.ACCOUNT_RESOURCE, account, Account.class);
271    }
272
273    /**
274     * Get Accounts
275     * <p>
276     * Returns information about all accounts.
277     *
278     * @return Accounts on success, null otherwise
279     */
280    public Accounts getAccounts() {
281        return doGET(Accounts.ACCOUNTS_RESOURCE, Accounts.class, new QueryParams());
282    }
283
284    /**
285     * Get Accounts given query params
286     * <p>
287     * Returns information about all accounts.
288     *
289     * @param params {@link QueryParams}
290     * @return Accounts on success, null otherwise
291     */
292    public Accounts getAccounts(final QueryParams params) {
293        return doGET(Accounts.ACCOUNTS_RESOURCE, Accounts.class, params);
294    }
295
296    /**
297     * Get number of Accounts matching the query params
298     *
299     * @param params {@link QueryParams}
300     * @return Integer on success, null otherwise
301     */
302    public Integer getAccountsCount(final QueryParams params) {
303        HeaderGroup map = doHEAD(Accounts.ACCOUNTS_RESOURCE, params);
304        return Integer.parseInt(map.getFirstHeader(X_RECORDS_HEADER_NAME).getValue());
305    }
306
307    /**
308     * Get Coupons
309     * <p>
310     * Returns information about all accounts.
311     *
312     * @return Coupons on success, null otherwise
313     */
314    public Coupons getCoupons() {
315        return doGET(Coupons.COUPONS_RESOURCE, Coupons.class, new QueryParams());
316    }
317
318    /**
319     * Get Coupons given query params
320     * <p>
321     * Returns information about all accounts.
322     *
323     * @param params {@link QueryParams}
324     * @return Coupons on success, null otherwise
325     */
326    public Coupons getCoupons(final QueryParams params) {
327        return doGET(Coupons.COUPONS_RESOURCE, Coupons.class, params);
328    }
329
330    /**
331     * Get number of Coupons matching the query params
332     *
333     * @param params {@link QueryParams}
334     * @return Integer on success, null otherwise
335     */
336    public Integer getCouponsCount(final QueryParams params) {
337        HeaderGroup map = doHEAD(Coupons.COUPONS_RESOURCE, params);
338        return Integer.parseInt(map.getFirstHeader(X_RECORDS_HEADER_NAME).getValue());
339    }
340
341    /**
342     * Get Account
343     * <p>
344     * Returns information about a single account.
345     *
346     * @param accountCode recurly account id
347     * @return account object on success, null otherwise
348     */
349    public Account getAccount(final String accountCode) {
350        if (accountCode == null || accountCode.isEmpty())
351            throw new RuntimeException("accountCode cannot be empty!");
352
353        return doGET(Account.ACCOUNT_RESOURCE + "/" + urlEncode(accountCode), Account.class);
354    }
355
356    /**
357     * Update Account
358     * <p>
359     * Updates an existing account.
360     *
361     * @param accountCode recurly account id
362     * @param account     account object
363     * @return the updated account object on success, null otherwise
364     */
365    public Account updateAccount(final String accountCode, final Account account) {
366        return doPUT(Account.ACCOUNT_RESOURCE + "/" + urlEncode(accountCode), account, Account.class);
367    }
368
369    /**
370     * Get Account Balance
371     * <p>
372     * Retrieves the remaining balance on the account
373     *
374     * @param accountCode recurly account id
375     * @return the updated AccountBalance if success, null otherwise
376     */
377    public AccountBalance getAccountBalance(final String accountCode) {
378        return doGET(Account.ACCOUNT_RESOURCE + "/" + urlEncode(accountCode) + AccountBalance.ACCOUNT_BALANCE_RESOURCE, AccountBalance.class);
379    }
380
381    /**
382     * Close Account
383     * <p>
384     * Marks an account as closed and cancels any active subscriptions. Any saved billing information will also be
385     * permanently removed from the account.
386     *
387     * @param accountCode recurly account id
388     */
389    public void closeAccount(final String accountCode) {
390        doDELETE(Account.ACCOUNT_RESOURCE + "/" + urlEncode(accountCode));
391    }
392
393    /**
394     * Reopen Account
395     * <p>
396     * Transitions a closed account back to active.
397     *
398     * @param accountCode recurly account id
399     */
400    public Account reopenAccount(final String accountCode) {
401        return doPUT(Account.ACCOUNT_RESOURCE + "/" + urlEncode(accountCode) + "/reopen",
402                     null, Account.class);
403    }
404
405
406    /**
407     * Get Child Accounts
408     * <p>
409     * Returns information about a the child accounts of an account.
410     *
411     * @param accountCode recurly account id
412     * @return Accounts on success, null otherwise
413     */
414    public Accounts getChildAccounts(final String accountCode) {
415        return doGET(Account.ACCOUNT_RESOURCE + "/" + urlEncode(accountCode) + "/child_accounts", Accounts.class, new QueryParams());
416    }
417
418    ////////////////////////////////////////////////////////////////////////////////////////
419    // Account adjustments
420
421    /**
422     * Get Account Adjustments
423     * <p>
424     *
425     * @param accountCode recurly account id
426     * @return the adjustments on the account
427     */
428    public Adjustments getAccountAdjustments(final String accountCode) {
429        return getAccountAdjustments(accountCode, null, null, new QueryParams());
430    }
431
432    /**
433     * Get Account Adjustments
434     * <p>
435     *
436     * @param accountCode recurly account id
437     * @param type {@link com.ning.billing.recurly.model.Adjustments.AdjustmentType}
438     * @return the adjustments on the account
439     */
440    public Adjustments getAccountAdjustments(final String accountCode, final Adjustments.AdjustmentType type) {
441        return getAccountAdjustments(accountCode, type, null, new QueryParams());
442    }
443
444    /**
445     * Get Account Adjustments
446     * <p>
447     *
448     * @param accountCode recurly account id
449     * @param type {@link com.ning.billing.recurly.model.Adjustments.AdjustmentType}
450     * @param state {@link com.ning.billing.recurly.model.Adjustments.AdjustmentState}
451     * @return the adjustments on the account
452     */
453    public Adjustments getAccountAdjustments(final String accountCode, final Adjustments.AdjustmentType type, final Adjustments.AdjustmentState state) {
454        return getAccountAdjustments(accountCode, type, state, new QueryParams());
455    }
456
457    /**
458     * Get Account Adjustments
459     * <p>
460     *
461     * @param accountCode recurly account id
462     * @param type {@link com.ning.billing.recurly.model.Adjustments.AdjustmentType}
463     * @param state {@link com.ning.billing.recurly.model.Adjustments.AdjustmentState}
464     * @param params {@link QueryParams}
465     * @return the adjustments on the account
466     */
467    public Adjustments getAccountAdjustments(final String accountCode, final Adjustments.AdjustmentType type, final Adjustments.AdjustmentState state, final QueryParams params) {
468        final String url = Account.ACCOUNT_RESOURCE + "/" + urlEncode(accountCode) + Adjustments.ADJUSTMENTS_RESOURCE;
469
470        if (type != null) params.put("type", type.getType());
471        if (state != null) params.put("state", state.getState());
472
473        return doGET(url, Adjustments.class, params);
474    }
475
476    public Adjustment getAdjustment(final String adjustmentUuid) {
477        if (adjustmentUuid == null || adjustmentUuid.isEmpty())
478            throw new RuntimeException("adjustmentUuid cannot be empty!");
479
480        return doGET(Adjustments.ADJUSTMENTS_RESOURCE + "/" + urlEncode(adjustmentUuid), Adjustment.class);
481    }
482
483    public Adjustment createAccountAdjustment(final String accountCode, final Adjustment adjustment) {
484        return doPOST(Account.ACCOUNT_RESOURCE + "/" + urlEncode(accountCode) + Adjustments.ADJUSTMENTS_RESOURCE,
485                      adjustment,
486                      Adjustment.class);
487    }
488
489    public void deleteAccountAdjustment(final String accountCode) {
490        doDELETE(Account.ACCOUNT_RESOURCE + "/" + urlEncode(accountCode) + Adjustments.ADJUSTMENTS_RESOURCE);
491    }
492
493    public void deleteAdjustment(final String adjustmentUuid) {
494        doDELETE(Adjustments.ADJUSTMENTS_RESOURCE + "/" + urlEncode(adjustmentUuid));
495    }
496
497    ////////////////////////////////////////////////////////////////////////////////////////
498
499    /**
500     * Create a subscription
501     * <p>
502     * Creates a subscription for an account.
503     *
504     * @param subscription Subscription object
505     * @return the newly created Subscription object on success, null otherwise
506     */
507    public Subscription createSubscription(final Subscription subscription) {
508        return doPOST(Subscription.SUBSCRIPTION_RESOURCE,
509                      subscription, Subscription.class);
510    }
511
512    /**
513     * Preview a subscription
514     * <p>
515     * Previews a subscription for an account.
516     *
517     * @param subscription Subscription object
518     * @return the newly created Subscription object on success, null otherwise
519     */
520    public Subscription previewSubscription(final Subscription subscription) {
521        return doPOST(Subscription.SUBSCRIPTION_RESOURCE
522                      + "/preview",
523                      subscription, Subscription.class);
524    }
525
526    /**
527     * Get a particular {@link Subscription} by it's UUID
528     * <p>
529     * Returns information about a single subscription.
530     *
531     * @param uuid UUID of the subscription to lookup
532     * @return Subscription
533     */
534    public Subscription getSubscription(final String uuid) {
535        if (uuid == null || uuid.isEmpty())
536            throw new RuntimeException("uuid cannot be empty!");
537
538        return doGET(Subscriptions.SUBSCRIPTIONS_RESOURCE
539                     + "/" + urlEncode(uuid),
540                     Subscription.class);
541    }
542
543    /**
544     * Cancel a subscription
545     * <p>
546     * Cancel a subscription so it remains active and then expires at the end of the current bill cycle.
547     *
548     * @param subscription Subscription object
549     * @return Subscription
550     */
551    public Subscription cancelSubscription(final Subscription subscription) {
552        return doPUT(Subscription.SUBSCRIPTION_RESOURCE + "/" + urlEncode(subscription.getUuid()) + "/cancel",
553                     subscription, Subscription.class);
554    }
555
556    /**
557     * Cancel a subscription
558     * <p>
559     * Cancel a subscription so it remains active and then expires at the end of the current bill cycle.
560     *
561     * @param subscriptionUuid String uuid of the subscription to cancel
562     * @param timeframe SubscriptionUpdate.TimeFrame the timeframe in which to cancel. Only accepts bill_date or term_end
563     * @return Subscription
564     */
565    public Subscription cancelSubscription(final String subscriptionUuid, final SubscriptionUpdate.Timeframe timeframe) {
566        final QueryParams qp = new QueryParams();
567        if (timeframe != null) qp.put("timeframe", timeframe.toString());
568        return doPUT(Subscription.SUBSCRIPTION_RESOURCE + "/" + urlEncode(subscriptionUuid) + "/cancel",
569                     null, Subscription.class, qp);
570    }
571
572    /**
573     * Pause a subscription or cancel a scheduled pause on a subscription.
574     * <p>
575     * * For an active subscription without a pause scheduled already, this will
576     *   schedule a pause period to begin at the next renewal date for the specified
577     *   number of billing cycles (remaining_pause_cycles).
578     * * When a scheduled pause already exists, this will update the remaining pause
579     *   cycles with the new value sent. When zero (0) remaining_pause_cycles is sent
580     *   for a subscription with a scheduled pause, the pause will be canceled.
581     * * For a paused subscription, the remaining_pause_cycles will adjust the
582     *   length of the current pause period. Sending zero (0) in the remaining_pause_cycles
583     *   field will cause the subscription to be resumed at the next renewal date.
584     *
585     * @param subscriptionUuid The uuid for the subscription you wish to pause.
586     * @param remainingPauseCycles The number of billing cycles that the subscription will be paused.
587     * @return Subscription
588     */
589    public Subscription pauseSubscription(final String subscriptionUuid, final int remainingPauseCycles) {
590        Subscription request = new Subscription();
591        request.setRemainingPauseCycles(remainingPauseCycles);
592        return doPUT(Subscription.SUBSCRIPTION_RESOURCE + "/" + urlEncode(subscriptionUuid) + "/pause",
593                     request, Subscription.class);
594    }
595
596    /**
597     * Convert trial to paid subscription when TransactionType = "moto".
598     * @param subscriptionUuid The uuid for the subscription you want to convert from trial to paid.
599     * @return Subscription
600     */
601    public Subscription convertTrialMoto(final String subscriptionUuid) {
602        Subscription request = new Subscription();
603        request.setTransactionType("moto");
604        return doPUT(Subscription.SUBSCRIPTION_RESOURCE + "/" + urlEncode(subscriptionUuid) + "/convert_trial",
605            request, Subscription.class);
606    }
607
608    /**
609     * Convert trial to paid subscription without 3DS token
610     * @param subscriptionUuid The uuid for the subscription you want to convert from trial to paid.
611     * @return Subscription
612     */
613    public Subscription convertTrial(final String subscriptionUuid) {
614        return convertTrial(subscriptionUuid, null);
615    }
616
617    /**
618     * Convert trial to paid subscription with 3DS token
619     * @param subscriptionUuid The uuid for the subscription you want to convert from trial to paid.
620     * @param ThreeDSecureActionResultTokenId 3DS secure action result token id in billing info.
621     * @return Subscription
622     */
623    public Subscription convertTrial(final String subscriptionUuid, final String ThreeDSecureActionResultTokenId) {
624        Subscription request;
625        if (ThreeDSecureActionResultTokenId == null) {
626            request = null;
627        } else {
628            request = new Subscription();
629            Account account = new Account();
630            BillingInfo billingInfo = new BillingInfo();
631            billingInfo.setThreeDSecureActionResultTokenId(ThreeDSecureActionResultTokenId);
632            account.setBillingInfo(billingInfo);
633            request.setAccount(account);
634        }
635        return doPUT(Subscription.SUBSCRIPTION_RESOURCE + "/" + urlEncode(subscriptionUuid) + "/convert_trial",
636            request, Subscription.class);
637    }
638
639    /**
640     * Immediately resumes a currently paused subscription.
641     * <p>
642     * For a paused subscription, this will immediately resume the subscription
643     * from the pause, produce an invoice, and return the newly resumed subscription.
644     * Any at-renewal subscription changes will be immediately applied when
645     * the subscription resumes.
646     *
647     * @param subscriptionUuid The uuid for the subscription you wish to pause.
648     * @return Subscription
649     */
650    public Subscription resumeSubscription(final String subscriptionUuid) {
651        return doPUT(Subscription.SUBSCRIPTION_RESOURCE + "/" + urlEncode(subscriptionUuid) + "/resume",
652                null, Subscription.class);
653    }
654
655    /**
656     * Postpone a subscription
657     * <p>
658     * postpone a subscription, setting a new renewal date.
659     *
660     * @param subscription Subscription object
661     * @return Subscription
662     */
663    public Subscription postponeSubscription(final Subscription subscription, final DateTime nextBillDate) {
664        final QueryParams params = new QueryParams();
665        params.put("next_bill_date", nextBillDate.toString());
666        return doPUT(Subscription.SUBSCRIPTION_RESOURCE + "/" + urlEncode(subscription.getUuid()) + "/postpone",
667                     subscription, Subscription.class, params);
668    }
669
670    /**
671     * Terminate a particular {@link Subscription} by it's UUID
672     *
673     * @param subscription Subscription to terminate
674     */
675    public void terminateSubscription(final Subscription subscription, final RefundOption refund) {
676        final QueryParams params = new QueryParams();
677        params.put("refund", refund.toString());
678        doPUT(Subscription.SUBSCRIPTION_RESOURCE + "/" + urlEncode(subscription.getUuid()) + "/terminate",
679              subscription, Subscription.class, params);
680    }
681
682    /**
683     * Reactivating a canceled subscription
684     * <p>
685     * Reactivate a canceled subscription so it renews at the end of the current bill cycle.
686     *
687     * @param subscription Subscription object
688     * @return Subscription
689     */
690    public Subscription reactivateSubscription(final Subscription subscription) {
691        return doPUT(Subscription.SUBSCRIPTION_RESOURCE + "/" + urlEncode(subscription.getUuid()) + "/reactivate",
692                     subscription, Subscription.class);
693    }
694
695    /**
696     * Update a particular {@link Subscription} by it's UUID
697     * <p>
698     * Returns information about a single subscription.
699     *
700     * @param uuid               UUID of the subscription to update
701     * @param subscriptionUpdate subscriptionUpdate object
702     * @return Subscription the updated subscription
703     */
704    public Subscription updateSubscription(final String uuid, final SubscriptionUpdate subscriptionUpdate) {
705        return doPUT(Subscriptions.SUBSCRIPTIONS_RESOURCE
706                     + "/" + urlEncode(uuid),
707                     subscriptionUpdate,
708                     Subscription.class);
709    }
710
711    /**
712     * Preview an update to a particular {@link Subscription} by it's UUID
713     * <p>
714     * Returns information about a single subscription.
715     *
716     * @param uuid UUID of the subscription to preview an update for
717     * @return Subscription the updated subscription preview
718     */
719    public Subscription updateSubscriptionPreview(final String uuid, final SubscriptionUpdate subscriptionUpdate) {
720        return doPOST(Subscriptions.SUBSCRIPTIONS_RESOURCE
721                      + "/" + urlEncode(uuid) + "/preview",
722                      subscriptionUpdate,
723                      Subscription.class);
724    }
725
726
727    /**
728     * Update to a particular {@link Subscription}'s notes by it's UUID
729     * <p>
730     * Returns information about a single subscription.
731     *
732     * @param uuid UUID of the subscription to preview an update for
733     * @param subscriptionNotes SubscriptionNotes object
734     * @return Subscription the updated subscription
735     */
736    public Subscription updateSubscriptionNotes(final String uuid, final SubscriptionNotes subscriptionNotes) {
737      return doPUT(SubscriptionNotes.SUBSCRIPTION_RESOURCE + "/" + urlEncode(uuid) + "/notes",
738                   subscriptionNotes, Subscription.class);
739    }
740
741    /**
742     * Get the subscriptions for an {@link Account}.
743     * <p>
744     * Returns subscriptions associated with an account
745     *
746     * @param accountCode recurly account id
747     * @return Subscriptions on the account
748     */
749    public Subscriptions getAccountSubscriptions(final String accountCode) {
750        return doGET(Account.ACCOUNT_RESOURCE
751                     + "/" + urlEncode(accountCode)
752                     + Subscriptions.SUBSCRIPTIONS_RESOURCE,
753                     Subscriptions.class,
754                     new QueryParams());
755    }
756
757    /**
758     * Get all the subscriptions on the site
759     * <p>
760     * Returns all the subscriptions on the site
761     *
762     * @return Subscriptions on the site
763     */
764    public Subscriptions getSubscriptions() {
765        return doGET(Subscriptions.SUBSCRIPTIONS_RESOURCE,
766                Subscriptions.class, new QueryParams());
767    }
768
769    /**
770     * Get all the subscriptions on the site given some sort and filter params.
771     * <p>
772     * Returns all the subscriptions on the site
773     *
774     * @param state {@link SubscriptionState}
775     * @param params {@link QueryParams}
776     * @return Subscriptions on the site
777     */
778    public Subscriptions getSubscriptions(final SubscriptionState state, final QueryParams params) {
779        if (state != null) { params.put("state", state.getType()); }
780
781        return doGET(Subscriptions.SUBSCRIPTIONS_RESOURCE,
782                Subscriptions.class, params);
783    }
784
785    /**
786     * Get number of Subscriptions matching the query params
787     *
788     * @param params {@link QueryParams}
789     * @return Integer on success, null otherwise
790     */
791    public Integer getSubscriptionsCount(final QueryParams params) {
792        HeaderGroup map = doHEAD(Subscription.SUBSCRIPTION_RESOURCE,  params);
793        return Integer.parseInt(map.getFirstHeader(X_RECORDS_HEADER_NAME).getValue());
794    }
795
796    /**
797     * Get the subscriptions for an {@link Account} given query params
798     * <p>
799     * Returns subscriptions associated with an account
800     *
801     * @param accountCode recurly account id
802     * @param state {@link SubscriptionState}
803     * @param params {@link QueryParams}
804     * @return Subscriptions on the account
805     */
806    public Subscriptions getAccountSubscriptions(final String accountCode, final SubscriptionState state, final QueryParams params) {
807        if (state != null) params.put("state", state.getType());
808
809        return doGET(Account.ACCOUNT_RESOURCE
810                        + "/" + urlEncode(accountCode)
811                        + Subscriptions.SUBSCRIPTIONS_RESOURCE,
812                Subscriptions.class,
813                params);
814    }
815
816    /**
817     * Return all the subscriptions on an invoice.
818     *
819     * @param invoiceId String Recurly Invoice ID
820     * @return all the subscriptions on the invoice
821     */
822    public Subscriptions getInvoiceSubscriptions(final String invoiceId) {
823        return getInvoiceSubscriptions(invoiceId, new QueryParams());
824    }
825
826    /**
827     * Return all the subscriptions on an invoice given query params.
828     *
829     * @param invoiceId String Recurly Invoice ID
830     * @param params {@link QueryParams}
831     * @return all the subscriptions on the invoice
832     */
833    public Subscriptions getInvoiceSubscriptions(final String invoiceId, final QueryParams params) {
834        return doGET(Invoices.INVOICES_RESOURCE
835                        + "/" + urlEncode(invoiceId)
836                        + Subscriptions.SUBSCRIPTIONS_RESOURCE,
837                Subscriptions.class,
838                params);
839    }
840
841    /**
842     * Post usage to subscription
843     * <p>
844     *
845     * @param subscriptionCode The recurly id of the {@link Subscription }
846     * @param addOnCode recurly id of {@link AddOn}
847     * @param usage the usage to post on recurly
848     * @return the {@link Usage} object as identified by the passed in object
849     */
850    public Usage postSubscriptionUsage(final String subscriptionCode, final String addOnCode, final Usage usage) {
851        return doPOST(Subscription.SUBSCRIPTION_RESOURCE +
852                        "/" +
853                        urlEncode(subscriptionCode) +
854                        AddOn.ADDONS_RESOURCE +
855                        "/" +
856                        urlEncode(addOnCode) +
857                        Usage.USAGE_RESOURCE,
858                usage, Usage.class);
859    }
860
861    /**
862     * Get Subscription Addon Usages
863     * <p>
864     *
865     * @param subscriptionCode The recurly id of the {@link Subscription }
866     * @param addOnCode recurly id of {@link AddOn}
867     * @return {@link Usages} for the specified subscription and addOn
868     */
869    public Usages getSubscriptionUsages(final String subscriptionCode, final String addOnCode, final QueryParams params) {
870       return doGET(Subscription.SUBSCRIPTION_RESOURCE +
871                        "/" +
872                        urlEncode(subscriptionCode) +
873                        AddOn.ADDONS_RESOURCE +
874                        "/" +
875                        urlEncode(addOnCode) +
876                        Usage.USAGE_RESOURCE, Usages.class, params );
877    }
878
879
880    /**
881     * Get the subscriptions for an account.
882     * This is deprecated. Please use getAccountSubscriptions(String, Subscriptions.State, QueryParams)
883     * <p>
884     * Returns information about a single account.
885     *
886     * @param accountCode recurly account id
887     * @param status      Only accounts in this status will be returned
888     * @return Subscriptions on the account
889     */
890    @Deprecated
891    public Subscriptions getAccountSubscriptions(final String accountCode, final String status) {
892        final QueryParams params = new QueryParams();
893        if (status != null) params.put("state", status);
894
895        return doGET(Account.ACCOUNT_RESOURCE
896                        + "/" + urlEncode(accountCode)
897                        + Subscriptions.SUBSCRIPTIONS_RESOURCE,
898                Subscriptions.class, params);
899    }
900
901    ////////////////////////////////////////////////////////////////////////////////////////
902
903    /**
904     * Update an account's billing info
905     * <p>
906     * When new or updated credit card information is updated, the billing information is only saved if the credit card
907     * is valid. If the account has a past due invoice, the outstanding balance will be collected to validate the
908     * billing information.
909     * <p>
910     * If the account does not exist before the API request, the account will be created if the billing information
911     * is valid.
912     * <p>
913     * Please note: this API end-point may be used to import billing information without security codes (CVV).
914     * Recurly recommends requiring CVV from your customers when collecting new or updated billing information.
915     *
916     * @param accountCode recurly account id
917     * @param billingInfo billing info object to create or update
918     * @return the newly created or update billing info object on success, null otherwise
919     */
920    public BillingInfo createOrUpdateBillingInfo(final String accountCode, final BillingInfo billingInfo) {
921        return doPUT(Account.ACCOUNT_RESOURCE + "/" + urlEncode(accountCode) + BillingInfo.BILLING_INFO_RESOURCE,
922                     billingInfo, BillingInfo.class);
923    }
924
925    public BillingInfo createBillingInfo(final String accountCode, final BillingInfo billingInfo) {
926        return doPOST(Account.ACCOUNT_RESOURCE + "/" + urlEncode(accountCode) + BillingInfos.BILLING_INFOS_RESOURCE,
927                     billingInfo, BillingInfo.class);
928    }
929
930    public BillingInfo updateBillingInfo(final String accountCode, final String uuid, final BillingInfo billingInfo) {
931        return doPUT(Account.ACCOUNT_RESOURCE + "/" + urlEncode(accountCode) + BillingInfos.BILLING_INFOS_RESOURCE + "/" + urlEncode(uuid),
932                     billingInfo, BillingInfo.class);
933    }
934
935    public BillingInfo getBillingInfoByUuid(final String accountCode, final String uuid) {
936        return doGET(Account.ACCOUNT_RESOURCE + "/" + urlEncode(accountCode) + BillingInfos.BILLING_INFOS_RESOURCE + "/" + urlEncode(uuid),
937                     BillingInfo.class);
938    }
939
940    public BillingInfos getBillingInfos(final String accountCode) {
941        return doGET(Account.ACCOUNT_RESOURCE + "/" + urlEncode(accountCode) + BillingInfos.BILLING_INFOS_RESOURCE,
942                  BillingInfos.class);
943    }
944
945    public void deleteBillingInfo(final String accountCode, final String uuid) {
946        doDELETE(Account.ACCOUNT_RESOURCE + "/" + urlEncode(accountCode) + BillingInfos.BILLING_INFOS_RESOURCE + "/" + uuid);
947    }
948
949    /**
950     * Update an account's billing info
951     * <p>
952     * When new or updated credit card information is updated, the billing information is only saved if the credit card
953     * is valid. If the account has a past due invoice, the outstanding balance will be collected to validate the
954     * billing information.
955     * <p>
956     * If the account does not exist before the API request, the account will be created if the billing information
957     * is valid.
958     * <p>
959     * Please note: this API end-point may be used to import billing information without security codes (CVV).
960     * Recurly recommends requiring CVV from your customers when collecting new or updated billing information.
961     *
962     * @deprecated Replaced by {@link #createOrUpdateBillingInfo(String, BillingInfo)} Please pass in the account code rather than setting the account on the BillingInfo object
963     *
964     * @param billingInfo billing info object to create or update
965     * @return the newly created or update billing info object on success, null otherwise
966     */
967    @Deprecated
968    public BillingInfo createOrUpdateBillingInfo(final BillingInfo billingInfo) {
969        final String accountCode = billingInfo.getAccount().getAccountCode();
970        // Unset it to avoid confusing Recurly
971        billingInfo.setAccount(null);
972        return doPUT(Account.ACCOUNT_RESOURCE + "/" + urlEncode(accountCode) + BillingInfo.BILLING_INFO_RESOURCE,
973                     billingInfo, BillingInfo.class);
974    }
975
976    /**
977     * Lookup an account's billing info
978     * <p>
979     * Returns only the account's current billing information.
980     *
981     * @param accountCode recurly account id
982     * @return the current billing info object associated with this account on success, null otherwise
983     */
984    public BillingInfo getBillingInfo(final String accountCode) {
985        return doGET(Account.ACCOUNT_RESOURCE + "/" + urlEncode(accountCode) + BillingInfo.BILLING_INFO_RESOURCE,
986                     BillingInfo.class);
987    }
988
989    /**
990     * Clear an account's billing info
991     * <p>
992     * You may remove any stored billing information for an account. If the account has a subscription, the renewal will
993     * go into past due unless you update the billing info before the renewal occurs
994     *
995     * @param accountCode recurly account id
996     */
997    public void clearBillingInfo(final String accountCode) {
998        doDELETE(Account.ACCOUNT_RESOURCE + "/" + urlEncode(accountCode) + BillingInfo.BILLING_INFO_RESOURCE);
999    }
1000
1001    /**
1002     * Verify an account's billing info
1003     * <p>
1004     * Verifies an account's billing info without providing a specific gateway. 
1005     * @param accountCode recurly account id
1006     * @return the transaction generated from the verification
1007     */
1008
1009    public Transaction verifyBillingInfo(final String accountCode) {
1010      final String url = Account.ACCOUNT_RESOURCE + "/" + urlEncode(accountCode) + BillingInfo.BILLING_INFO_RESOURCE + "/verify";
1011      final BillingInfoVerification gateway = new BillingInfoVerification();
1012      return doPOST(url, gateway, Transaction.class);
1013    }
1014
1015    /**
1016     * Verify an account's billing info
1017     * <p>
1018     * Verifies an account's billing info using a gateway code param. 
1019     * @param accountCode recurly account id
1020     * @param gatewayVerification BillingInfoVerification object used to verify billing info
1021     * @return the transaction generated from the verification
1022     */
1023
1024    public Transaction verifyBillingInfo(final String accountCode, final BillingInfoVerification gatewayVerification) {
1025      final String url = Account.ACCOUNT_RESOURCE + "/" + urlEncode(accountCode) + BillingInfo.BILLING_INFO_RESOURCE + "/verify";
1026      return doPOST(url, gatewayVerification, Transaction.class);
1027    }
1028
1029    ///////////////////////////////////////////////////////////////////////////
1030    // Account Notes
1031
1032    /**
1033     * List an account's notes
1034     * <p>
1035     * Returns the account's notes
1036     *
1037     * @param accountCode recurly account id
1038     * @return the notes associated with this account on success, null otherwise
1039     */
1040    public AccountNotes getAccountNotes(final String accountCode) {
1041        return doGET(Accounts.ACCOUNTS_RESOURCE + "/" + urlEncode(accountCode) + AccountNotes.ACCOUNT_NOTES_RESOURCE,
1042                     AccountNotes.class, new QueryParams());
1043    }
1044
1045    ///////////////////////////////////////////////////////////////////////////
1046    // User transactions
1047
1048    /**
1049     * Lookup an account's transactions history
1050     * <p>
1051     * Returns the account's transaction history
1052     *
1053     * @param accountCode recurly account id
1054     * @return the transaction history associated with this account on success, null otherwise
1055     */
1056    public Transactions getAccountTransactions(final String accountCode) {
1057        return doGET(Accounts.ACCOUNTS_RESOURCE + "/" + urlEncode(accountCode) + Transactions.TRANSACTIONS_RESOURCE,
1058                     Transactions.class, new QueryParams());
1059    }
1060
1061    /**
1062     * Lookup an account's transactions history given query params
1063     * <p>
1064     * Returns the account's transaction history
1065     *
1066     * @param accountCode recurly account id
1067     * @param state {@link TransactionState}
1068     * @param type {@link TransactionType}
1069     * @param params {@link QueryParams}
1070     * @return the transaction history associated with this account on success, null otherwise
1071     */
1072    public Transactions getAccountTransactions(final String accountCode, final TransactionState state, final TransactionType type, final QueryParams params) {
1073        if (state != null) params.put("state", state.getType());
1074        if (type != null) params.put("type", type.getType());
1075
1076        return doGET(Accounts.ACCOUNTS_RESOURCE + "/" + urlEncode(accountCode) + Transactions.TRANSACTIONS_RESOURCE,
1077                Transactions.class, params);
1078    }
1079
1080    /**
1081     * Get site's transaction history
1082     * <p>
1083     * All transactions on the site
1084     *
1085     * @return the transaction history of the site on success, null otherwise
1086     */
1087    public Transactions getTransactions() {
1088        return doGET(Transactions.TRANSACTIONS_RESOURCE, Transactions.class, new QueryParams());
1089    }
1090
1091    /**
1092     * Get site's transaction history
1093     * <p>
1094     * All transactions on the site
1095     *
1096     * @param state {@link TransactionState}
1097     * @param type {@link TransactionType}
1098     * @param params {@link QueryParams}
1099     * @return the transaction history of the site on success, null otherwise
1100     */
1101    public Transactions getTransactions(final TransactionState state, final TransactionType type, final QueryParams params) {
1102        if (state != null) params.put("state", state.getType());
1103        if (type != null) params.put("type", type.getType());
1104
1105        return doGET(Transactions.TRANSACTIONS_RESOURCE, Transactions.class, params);
1106    }
1107
1108    /**
1109     * Get number of Transactions matching the query params
1110     *
1111     * @param params {@link QueryParams}
1112     * @return Integer on success, null otherwise
1113     */
1114    public Integer getTransactionsCount(final QueryParams params) {
1115        HeaderGroup map = doHEAD(Transactions.TRANSACTIONS_RESOURCE, params);
1116        return Integer.parseInt(map.getFirstHeader(X_RECORDS_HEADER_NAME).getValue());
1117    }
1118
1119    /**
1120     * Lookup a transaction
1121     *
1122     * @param transactionId recurly transaction id
1123     * @return the transaction if found, null otherwise
1124     */
1125    public Transaction getTransaction(final String transactionId) {
1126        if (transactionId == null || transactionId.isEmpty())
1127            throw new RuntimeException("transactionId cannot be empty!");
1128
1129        return doGET(Transactions.TRANSACTIONS_RESOURCE + "/" + urlEncode(transactionId),
1130                     Transaction.class);
1131    }
1132
1133    /**
1134     * Creates a {@link Transaction} through the Recurly API.
1135     *
1136     * @param trans The {@link Transaction} to create
1137     * @return The created {@link Transaction} object
1138     */
1139    public Transaction createTransaction(final Transaction trans) {
1140        return doPOST(Transactions.TRANSACTIONS_RESOURCE, trans, Transaction.class);
1141    }
1142
1143    /**
1144     * Refund a transaction
1145     *
1146     * @param transactionId recurly transaction id
1147     * @param amount        amount to refund, null for full refund
1148     */
1149    public void refundTransaction(final String transactionId, @Nullable final BigDecimal amount) {
1150        String url = Transactions.TRANSACTIONS_RESOURCE + "/" + urlEncode(transactionId);
1151        if (amount != null) {
1152            url = url + "?amount_in_cents=" + (amount.intValue() * 100);
1153        }
1154        doDELETE(url);
1155    }
1156
1157    /**
1158     * Get the subscriptions for a {@link Transaction}.
1159     * <p>
1160     * Returns subscriptions associated with a transaction
1161     *
1162     * @param transactionId recurly transaction id
1163     * @return Subscriptions on the transaction
1164     */
1165    public Subscriptions getTransactionSubscriptions(final String transactionId) {
1166        return doGET(Transactions.TRANSACTIONS_RESOURCE
1167                        + "/" + urlEncode(transactionId)
1168                        + Subscriptions.SUBSCRIPTIONS_RESOURCE,
1169                Subscriptions.class,
1170                new QueryParams());
1171    }
1172
1173    ///////////////////////////////////////////////////////////////////////////
1174    // User invoices
1175
1176    /**
1177     * Lookup an invoice
1178     * <p>
1179     * Returns the invoice given an integer id
1180     *
1181     * @deprecated Please switch to using a string for invoice ids
1182     *
1183     * @param invoiceId Recurly Invoice ID
1184     * @return the invoice
1185     */
1186    @Deprecated
1187    public Invoice getInvoice(final Integer invoiceId) {
1188        return getInvoice(invoiceId.toString());
1189    }
1190
1191    /**
1192     * Lookup an invoice given an invoice id
1193     *
1194     * <p>
1195     * Returns the invoice given a string id.
1196     * The invoice may or may not have acountry code prefix (ex: IE1023).
1197     * For more information on invoicing and prefixes, see:
1198     * https://docs.recurly.com/docs/site-settings#section-invoice-prefixing
1199     *
1200     * @param invoiceId String Recurly Invoice ID
1201     * @return the invoice
1202     */
1203    public Invoice getInvoice(final String invoiceId) {
1204        if (invoiceId == null || invoiceId.isEmpty())
1205            throw new RuntimeException("invoiceId cannot be empty!");
1206
1207        return doGET(Invoices.INVOICES_RESOURCE + "/" + urlEncode(invoiceId), Invoice.class);
1208    }
1209
1210    /**
1211     * Update an invoice
1212     * <p>
1213     * Updates an existing invoice.
1214     *
1215     * @param invoiceId String Recurly Invoice ID
1216     * @return the updated invoice object on success, null otherwise
1217     */
1218    public Invoice updateInvoice(final String invoiceId, final Invoice invoice) {
1219        return doPUT(Invoices.INVOICES_RESOURCE + "/" + urlEncode(invoiceId), invoice, Invoice.class);
1220    }
1221
1222    /**
1223     * Fetch invoice pdf
1224     * <p>
1225     * Returns the invoice pdf as an inputStream
1226     *
1227     * @deprecated Prefer using Invoice#getId() as the id param (which is a String)
1228     *
1229     * @param invoiceId Recurly Invoice ID
1230     * @return the invoice pdf as an inputStream
1231     */
1232    @Deprecated
1233    public InputStream getInvoicePdf(final Integer invoiceId) {
1234        return getInvoicePdf(invoiceId.toString());
1235    }
1236
1237    /**
1238     * Fetch invoice pdf
1239     * <p>
1240     * Returns the invoice pdf as an inputStream
1241     *
1242     * @param invoiceId String Recurly Invoice ID
1243     * @return the invoice pdf as an inputStream
1244     */
1245    public InputStream getInvoicePdf(final String invoiceId) {
1246        if (invoiceId == null || invoiceId.isEmpty())
1247            throw new RuntimeException("invoiceId cannot be empty!");
1248
1249        return doGETPdf(Invoices.INVOICES_RESOURCE + "/" + urlEncode(invoiceId));
1250    }
1251
1252    /**
1253     * Lookup all invoices
1254     * <p>
1255     * Returns all invoices on the site
1256     *
1257     * @return the invoices associated with this site on success, null otherwise
1258     */
1259    public Invoices getInvoices() {
1260        return doGET(Invoices.INVOICES_RESOURCE, Invoices.class, new QueryParams());
1261    }
1262
1263    /**
1264     * Return all the invoices given query params
1265     * <p>
1266     *
1267     * @param params {@link QueryParams}
1268     * @return all invoices matching the query
1269     */
1270    public Invoices getInvoices(final QueryParams params) {
1271        return doGET(Invoices.INVOICES_RESOURCE, Invoices.class, params);
1272    }
1273
1274    /**
1275     * Return all the invoices given query params
1276     * <p>
1277     *
1278     * @param params {@link QueryParams}
1279     * @return the count of invoices matching the query
1280     */
1281    public int getInvoicesCount(final QueryParams params) {
1282        HeaderGroup map = doHEAD(Invoices.INVOICES_RESOURCE, params);
1283        return Integer.parseInt(map.getFirstHeader(X_RECORDS_HEADER_NAME).getValue());
1284    }
1285
1286    /**
1287     * Return all the transactions on an invoice. Only use this endpoint
1288     * if you have more than 500 transactions on an invoice.
1289     * <p>
1290     *
1291     * @param invoiceId String Recurly Invoice ID
1292     * @return all the transactions on the invoice
1293     */
1294    public Transactions getInvoiceTransactions(final String invoiceId) {
1295        return doGET(Invoices.INVOICES_RESOURCE + "/" + urlEncode(invoiceId) + Transactions.TRANSACTIONS_RESOURCE,
1296                     Transactions.class, new QueryParams());
1297    }
1298
1299    /**
1300     * Lookup an account's invoices
1301     * <p>
1302     * Returns the account's invoices
1303     *
1304     * @param accountCode recurly account id
1305     * @return the invoices associated with this account on success, null otherwise
1306     */
1307    public Invoices getAccountInvoices(final String accountCode) {
1308        return doGET(Accounts.ACCOUNTS_RESOURCE + "/" + urlEncode(accountCode) + Invoices.INVOICES_RESOURCE,
1309                     Invoices.class, new QueryParams());
1310    }
1311
1312    /**
1313     * Lookup an invoice's original invoices (e.g. a refund invoice has original_invoices)
1314     * <p>
1315     * Returns the invoice's original invoices
1316     *
1317     * @param invoiceId the invoice id
1318     * @return the original invoices associated with this invoice on success. Throws RecurlyAPIError if not found
1319     */
1320    public Invoices getOriginalInvoices(final String invoiceId) {
1321        return doGET(Invoices.INVOICES_RESOURCE + "/" + urlEncode(invoiceId) + "/original_invoices",
1322                    Invoices.class, new QueryParams());
1323    }
1324
1325    /**
1326     * Refund an invoice given an open amount
1327     * <p/>
1328     * Returns the refunded invoice
1329     *
1330     * @deprecated Please use refundInvoice(String, InvoiceRefund)
1331     *
1332     * @param invoiceId The id of the invoice to refund
1333     * @param amountInCents The open amount to refund
1334     * @param method If credit line items exist on the invoice, this parameter specifies which refund method to use first
1335     * @return the refunded invoice
1336     */
1337    @Deprecated
1338    public Invoice refundInvoice(final String invoiceId, final Integer amountInCents, final RefundMethod method) {
1339        final InvoiceRefund invoiceRefund = new InvoiceRefund();
1340        invoiceRefund.setRefundMethod(method);
1341        invoiceRefund.setAmountInCents(amountInCents);
1342
1343        return refundInvoice(invoiceId, invoiceRefund);
1344    }
1345
1346    /**
1347     * Refund an invoice given some line items
1348     * <p/>
1349     * Returns the refunded invoice
1350     *
1351     * @deprecated Please use refundInvoice(String, InvoiceRefund)
1352     *
1353     * @param invoiceId The id of the invoice to refund
1354     * @param lineItems The list of adjustment refund objects
1355     * @param method If credit line items exist on the invoice, this parameter specifies which refund method to use first
1356     * @return the refunded invoice
1357     */
1358    @Deprecated
1359    public Invoice refundInvoice(final String invoiceId, List<AdjustmentRefund> lineItems, final RefundMethod method) {
1360        final InvoiceRefund invoiceRefund = new InvoiceRefund();
1361        invoiceRefund.setRefundMethod(method);
1362        invoiceRefund.setLineItems(lineItems);
1363
1364        return refundInvoice(invoiceId, invoiceRefund);
1365    }
1366
1367    /**
1368     * Refund an invoice given some options
1369     * <p/>
1370     * Returns the refunded invoice
1371     *
1372     * @param invoiceId The id of the invoice to refund
1373     * @param refundOptions The options for the refund
1374     * @return the refunded invoice
1375     */
1376    public Invoice refundInvoice(final String invoiceId, final InvoiceRefund refundOptions) {
1377        return doPOST(Invoices.INVOICES_RESOURCE + "/" + urlEncode(invoiceId) + "/refund", refundOptions, Invoice.class);
1378    }
1379
1380    /**
1381     * Lookup an account's shipping addresses
1382     * <p>
1383     * Returns the account's shipping addresses
1384     *
1385     * @param accountCode recurly account id
1386     * @return the shipping addresses associated with this account on success, null otherwise
1387     */
1388    public ShippingAddresses getAccountShippingAddresses(final String accountCode) {
1389        return doGET(Accounts.ACCOUNTS_RESOURCE + "/" + urlEncode(accountCode) + ShippingAddresses.SHIPPING_ADDRESSES_RESOURCE,
1390                ShippingAddresses.class, new QueryParams());
1391    }
1392
1393    /**
1394     * Get an existing shipping address
1395     * <p>
1396     *
1397     * @param accountCode recurly account id
1398     * @param shippingAddressId the shipping address id to fetch
1399     * @return the newly created shipping address on success
1400     */
1401    public ShippingAddress getShippingAddress(final String accountCode, final long shippingAddressId) {
1402        return doGET(Accounts.ACCOUNTS_RESOURCE + "/" + urlEncode(accountCode) + ShippingAddresses.SHIPPING_ADDRESSES_RESOURCE + "/" + shippingAddressId,
1403                ShippingAddress.class);
1404    }
1405
1406    /**
1407     * Create a shipping address on an existing account
1408     * <p>
1409     *
1410     * @param accountCode recurly account id
1411     * @param shippingAddress the shipping address request data
1412     * @return the newly created shipping address on success
1413     */
1414    public ShippingAddress createShippingAddress(final String accountCode, final ShippingAddress shippingAddress) {
1415        return doPOST(Accounts.ACCOUNTS_RESOURCE + "/" + urlEncode(accountCode) + ShippingAddresses.SHIPPING_ADDRESSES_RESOURCE, shippingAddress,
1416                ShippingAddress.class);
1417    }
1418
1419    /**
1420     * Update an existing shipping address
1421     * <p>
1422     *
1423     * @param accountCode recurly account id
1424     * @param shippingAddressId the shipping address id to update
1425     * @param shippingAddress the shipping address request data
1426     * @return the updated shipping address on success
1427     */
1428    public ShippingAddress updateShippingAddress(final String accountCode, final long shippingAddressId, ShippingAddress shippingAddress) {
1429        return doPUT(Accounts.ACCOUNTS_RESOURCE + "/" + urlEncode(accountCode) + ShippingAddresses.SHIPPING_ADDRESSES_RESOURCE + "/" + shippingAddressId, shippingAddress,
1430                ShippingAddress.class);
1431    }
1432
1433    /**
1434     * Delete an existing shipping address
1435     * <p>
1436     *
1437     * @param accountCode recurly account id
1438     * @param shippingAddressId the shipping address id to delete
1439     */
1440    public void deleteShippingAddress(final String accountCode, final long shippingAddressId) {
1441        doDELETE(Accounts.ACCOUNTS_RESOURCE + "/" + urlEncode(accountCode) + ShippingAddresses.SHIPPING_ADDRESSES_RESOURCE + "/" + shippingAddressId);
1442    }
1443
1444    /**
1445     * Lookup an account's invoices given query params
1446     * <p>
1447     * Returns the account's invoices
1448     *
1449     * @param accountCode recurly account id
1450     * @param state {@link InvoiceState} state of the invoices
1451     * @param params {@link QueryParams}
1452     * @return the invoices associated with this account on success, null otherwise
1453     */
1454    public Invoices getAccountInvoices(final String accountCode, final InvoiceState state, final QueryParams params) {
1455        if (state != null) params.put("state", state.getType());
1456        return doGET(Accounts.ACCOUNTS_RESOURCE + "/" + urlEncode(accountCode) + Invoices.INVOICES_RESOURCE,
1457                Invoices.class, params);
1458    }
1459
1460    /**
1461     * Post an invoice: invoice pending charges on an account
1462     * <p>
1463     * Returns an invoice collection
1464     *
1465     * @param accountCode
1466     * @return the invoice collection that was generated on success, null otherwise
1467     */
1468    public InvoiceCollection postAccountInvoice(final String accountCode, final Invoice invoice) {
1469        return doPOST(Accounts.ACCOUNTS_RESOURCE + "/" + urlEncode(accountCode) + Invoices.INVOICES_RESOURCE, invoice, InvoiceCollection.class);
1470    }
1471
1472    /**
1473     * Mark an invoice as paid successfully - Recurly Enterprise Feature
1474     *
1475     * @deprecated Prefer using Invoice#getId() as the id param (which is a String)
1476     *
1477     * @param invoiceId Recurly Invoice ID
1478     */
1479    @Deprecated
1480    public Invoice markInvoiceSuccessful(final Integer invoiceId) {
1481        return markInvoiceSuccessful(invoiceId.toString());
1482    }
1483
1484    /**
1485     * Mark an invoice as paid successfully - Recurly Enterprise Feature
1486     *
1487     * @param invoiceId String Recurly Invoice ID
1488     */
1489    public Invoice markInvoiceSuccessful(final String invoiceId) {
1490        return doPUT(Invoices.INVOICES_RESOURCE + "/" + urlEncode(invoiceId) + "/mark_successful", null, Invoice.class);
1491    }
1492
1493    /**
1494     * Mark an invoice as failed collection
1495     *
1496     * @deprecated Prefer using Invoice#getId() as the id param (which is a String)
1497     *
1498     * @param invoiceId Recurly Invoice ID
1499     */
1500    @Deprecated
1501    public InvoiceCollection markInvoiceFailed(final Integer invoiceId) {
1502        return markInvoiceFailed(invoiceId.toString());
1503    }
1504
1505    /**
1506     * Mark an invoice as failed collection
1507     *
1508     * @param invoiceId String Recurly Invoice ID
1509     */
1510    public InvoiceCollection markInvoiceFailed(final String invoiceId) {
1511        return doPUT(Invoices.INVOICES_RESOURCE + "/" + urlEncode(invoiceId) + "/mark_failed", null, InvoiceCollection.class);
1512    }
1513
1514    /**
1515     * Force collect an invoice
1516     *
1517     * @param invoiceId String Recurly Invoice ID
1518     */
1519    public Invoice forceCollectInvoice(final String invoiceId) {
1520        return doPUT(Invoices.INVOICES_RESOURCE + "/" + urlEncode(invoiceId) + "/collect", null, Invoice.class);
1521    }
1522
1523    /**
1524     * Force collect an invoice
1525     *
1526     * @param transactionType String The gateway transaction type. Currency accepts value "moto".
1527     * @param invoiceId String Recurly Invoice ID
1528     */
1529    public Invoice forceCollectInvoice(final String invoiceId, final String transactionType) {
1530        Invoice request = new Invoice();
1531        request.setTransactionType(transactionType);
1532        return doPUT(Invoices.INVOICES_RESOURCE + "/" + urlEncode(invoiceId) + "/collect", request, Invoice.class);
1533    }
1534
1535        /**
1536     * Force collect an invoice
1537     *
1538     * @param billingInfoUuid String The billing info uuid.
1539     * @param invoiceId String Recurly Invoice ID
1540     */
1541    public Invoice forceCollectInvoiceWithBillingInfo(final String invoiceId, final String billingInfoUuid) {
1542      Invoice request = new Invoice();
1543      request.setBillingInfoUuid(billingInfoUuid);
1544      return doPUT(Invoices.INVOICES_RESOURCE + "/" + urlEncode(invoiceId) + "/collect", request, Invoice.class);
1545  }
1546
1547    /**
1548     * Void Invoice
1549     *
1550     * @param invoiceId String Recurly Invoice ID
1551     */
1552    public Invoice voidInvoice(final String invoiceId) {
1553        return doPUT(Invoices.INVOICES_RESOURCE + "/" + urlEncode(invoiceId) + "/void", null, Invoice.class);
1554    }
1555
1556    /**
1557     * Enter an offline payment for a manual invoice (beta) - Recurly Enterprise Feature
1558     *
1559     * @deprecated Prefer using Invoice#getId() as the id param (which is a String)
1560     *
1561     * @param invoiceId Recurly Invoice ID
1562     * @param payment   The external payment
1563     */
1564    @Deprecated
1565    public Transaction enterOfflinePayment(final Integer invoiceId, final Transaction payment) {
1566        return enterOfflinePayment(invoiceId.toString(), payment);
1567    }
1568
1569    /**
1570     * Enter an offline payment for a manual invoice (beta) - Recurly Enterprise Feature
1571     *
1572     * @param invoiceId String Recurly Invoice ID
1573     * @param payment   The external payment
1574     */
1575    public Transaction enterOfflinePayment(final String invoiceId, final Transaction payment) {
1576        return doPOST(Invoices.INVOICES_RESOURCE + "/" + urlEncode(invoiceId) + "/transactions", payment, Transaction.class);
1577    }
1578
1579    ///////////////////////////////////////////////////////////////////////////
1580
1581    /**
1582     * Create an Item's info
1583     * <p>
1584     *
1585     * @param item The item to create on recurly
1586     * @return the item object as identified by the passed in ID
1587     */
1588    public Item createItem(final Item item) {
1589        return doPOST(Item.ITEMS_RESOURCE, item, Item.class);
1590    }
1591
1592    /**
1593     * Update an Item's info
1594     * <p>
1595     *
1596     * @param item The Item to update on recurly
1597     * @return the updated item object
1598     */
1599    public Item updateItem(final String itemCode, final Item item) {
1600        return doPUT(Item.ITEMS_RESOURCE + "/" + urlEncode(itemCode), item, Item.class);
1601    }
1602
1603    /**
1604     * Get a Item's details
1605     * <p>
1606     *
1607     * @param itemCode recurly id of item
1608     * @return the item object as identified by the passed in ID
1609     */
1610    public Item getItem(final String itemCode) {
1611        if (itemCode == null || itemCode.isEmpty())
1612            throw new RuntimeException("itemCode cannot be empty!");
1613
1614        return doGET(Item.ITEMS_RESOURCE + "/" + urlEncode(itemCode), Item.class);
1615    }
1616
1617    /**
1618     * Return all the items
1619     * <p>
1620     *
1621     * @return the item object as identified by the passed in ID
1622     */
1623    public Items getItems() {
1624        return doGET(Items.ITEMS_RESOURCE, Items.class, new QueryParams());
1625    }
1626
1627    /**
1628     * Deletes a {@link Item}
1629     * <p>
1630     *
1631     * @param itemCode The {@link Item} object to delete.
1632     */
1633    public void deleteItem(final String itemCode) {
1634        doDELETE(Item.ITEMS_RESOURCE +
1635                "/" +
1636                urlEncode(itemCode));
1637    }
1638
1639    /**
1640     * Reactivating a canceled item
1641     * <p>
1642     * Reactivate a canceled item.
1643     *
1644     * @param item Item object
1645     * @return Item
1646     */
1647    public Item reactivateItem(final String itemCode) {
1648        return doPUT(Item.ITEMS_RESOURCE + "/" + urlEncode(itemCode) + "/reactivate",
1649                null, Item.class);
1650    }
1651
1652    ///////////////////////////////////////////////////////////////////////////
1653
1654    /**
1655     * Create a Plan's info
1656     * <p>
1657     *
1658     * @param plan The plan to create on recurly
1659     * @return the plan object as identified by the passed in ID
1660     */
1661    public Plan createPlan(final Plan plan) {
1662        return doPOST(Plan.PLANS_RESOURCE, plan, Plan.class);
1663    }
1664
1665    /**
1666     * Update a Plan's info
1667     * <p>
1668     *
1669     * @param plan The plan to update on recurly
1670     * @return the updated plan object
1671     */
1672    public Plan updatePlan(final Plan plan) {
1673        return doPUT(Plan.PLANS_RESOURCE + "/" + urlEncode(plan.getPlanCode()), plan, Plan.class);
1674    }
1675
1676    /**
1677     * Get a Plan's details
1678     * <p>
1679     *
1680     * @param planCode recurly id of plan
1681     * @return the plan object as identified by the passed in ID
1682     */
1683    public Plan getPlan(final String planCode) {
1684        if (planCode == null || planCode.isEmpty())
1685            throw new RuntimeException("planCode cannot be empty!");
1686
1687        return doGET(Plan.PLANS_RESOURCE + "/" + urlEncode(planCode), Plan.class);
1688    }
1689
1690    /**
1691     * Return all the plans
1692     * <p>
1693     *
1694     * @return the plan object as identified by the passed in ID
1695     */
1696    public Plans getPlans() {
1697        return doGET(Plans.PLANS_RESOURCE, Plans.class, new QueryParams());
1698    }
1699
1700    /**
1701     * Return all the plans given query params
1702     * <p>
1703     *
1704     * @param params {@link QueryParams}
1705     * @return the plan object as identified by the passed in ID
1706     */
1707    public Plans getPlans(final QueryParams params) {
1708        return doGET(Plans.PLANS_RESOURCE, Plans.class, params);
1709    }
1710
1711    /**
1712     * Get number of Plans matching the query params
1713     *
1714     * @param params {@link QueryParams}
1715     * @return Integer on success, null otherwise
1716     */
1717    public Integer getPlansCount(final QueryParams params) {
1718        HeaderGroup map = doHEAD(Plans.PLANS_RESOURCE, params);
1719        return Integer.parseInt(map.getFirstHeader(X_RECORDS_HEADER_NAME).getValue());
1720    }
1721
1722    /**
1723     * Deletes a {@link Plan}
1724     * <p>
1725     *
1726     * @param planCode The {@link Plan} object to delete.
1727     */
1728    public void deletePlan(final String planCode) {
1729        doDELETE(Plan.PLANS_RESOURCE +
1730                 "/" +
1731                 urlEncode(planCode));
1732    }
1733
1734    ///////////////////////////////////////////////////////////////////////////
1735
1736    /**
1737     * Create an AddOn to a Plan
1738     * <p>
1739     *
1740     * @param planCode The planCode of the {@link Plan } to create within recurly
1741     * @param addOn    The {@link AddOn} to create within recurly
1742     * @return the {@link AddOn} object as identified by the passed in object
1743     */
1744    public AddOn createPlanAddOn(final String planCode, final AddOn addOn) {
1745        return doPOST(Plan.PLANS_RESOURCE +
1746                      "/" +
1747                      urlEncode(planCode) +
1748                      AddOn.ADDONS_RESOURCE,
1749                      addOn, AddOn.class);
1750    }
1751
1752    /**
1753     * Get an AddOn's details
1754     * <p>
1755     *
1756     * @param addOnCode recurly id of {@link AddOn}
1757     * @param planCode  recurly id of {@link Plan}
1758     * @return the {@link AddOn} object as identified by the passed in plan and add-on IDs
1759     */
1760    public AddOn getAddOn(final String planCode, final String addOnCode) {
1761        if (addOnCode == null || addOnCode.isEmpty())
1762            throw new RuntimeException("addOnCode cannot be empty!");
1763
1764        return doGET(Plan.PLANS_RESOURCE +
1765                     "/" +
1766                     urlEncode(planCode) +
1767                     AddOn.ADDONS_RESOURCE +
1768                     "/" +
1769                     addOnCode, AddOn.class);
1770    }
1771
1772    /**
1773     * Return all the {@link AddOn} for a {@link Plan}
1774     * <p>
1775     *
1776     * @param planCode
1777     * @return the {@link AddOn} objects as identified by the passed plan ID
1778     */
1779    public AddOns getAddOns(final String planCode) {
1780        return doGET(Plan.PLANS_RESOURCE +
1781                "/" +
1782                urlEncode(planCode) +
1783                AddOn.ADDONS_RESOURCE,
1784                AddOns.class,
1785                new QueryParams());
1786    }
1787
1788    /**
1789     * Return all the {@link AddOn} for a {@link Plan}
1790     * <p>
1791     *
1792     * @param planCode
1793     * @param params {@link QueryParams}
1794     * @return the {@link AddOn} objects as identified by the passed plan ID
1795     */
1796    public AddOns getAddOns(final String planCode, final QueryParams params) {
1797        return doGET(Plan.PLANS_RESOURCE +
1798                "/" +
1799                urlEncode(planCode) +
1800                AddOn.ADDONS_RESOURCE,
1801                AddOns.class,
1802                params);
1803    }
1804
1805    /**
1806     * Deletes an {@link AddOn} for a Plan
1807     * <p>
1808     *
1809     * @param planCode  The {@link Plan} object.
1810     * @param addOnCode The {@link AddOn} object to delete.
1811     */
1812    public void deleteAddOn(final String planCode, final String addOnCode) {
1813        doDELETE(Plan.PLANS_RESOURCE +
1814                 "/" +
1815                 urlEncode(planCode) +
1816                 AddOn.ADDONS_RESOURCE +
1817                 "/" +
1818                 urlEncode(addOnCode));
1819    }
1820
1821    /**
1822     * Updates an {@link AddOn} for a Plan
1823     * <p>
1824     *
1825     * @param planCode  The {@link Plan} object.
1826     * @param addOnCode The {@link AddOn} object to update.
1827     * @param addOn The updated {@link AddOn} data.
1828     *
1829     * @return the updated {@link AddOn} object.
1830     */
1831    public AddOn updateAddOn(final String planCode, final String addOnCode, final AddOn addOn) {
1832        return doPUT(Plan.PLANS_RESOURCE +
1833                "/" +
1834                urlEncode(planCode) +
1835                AddOn.ADDONS_RESOURCE +
1836                "/" +
1837                urlEncode(addOnCode),
1838                addOn,
1839                AddOn.class);
1840    }
1841
1842    ///////////////////////////////////////////////////////////////////////////
1843
1844    /**
1845     * Create a {@link Coupon}
1846     * <p>
1847     *
1848     * @param coupon The coupon to create on recurly
1849     * @return the {@link Coupon} object
1850     */
1851    public Coupon createCoupon(final Coupon coupon) {
1852        return doPOST(Coupon.COUPON_RESOURCE, coupon, Coupon.class);
1853    }
1854
1855    /**
1856     * Get a Coupon
1857     * <p>
1858     *
1859     * @param couponCode The code for the {@link Coupon}
1860     * @return The {@link Coupon} object as identified by the passed in code
1861     */
1862    public Coupon getCoupon(final String couponCode) {
1863        if (couponCode == null || couponCode.isEmpty())
1864            throw new RuntimeException("couponCode cannot be empty!");
1865
1866        return doGET(Coupon.COUPON_RESOURCE + "/" + urlEncode(couponCode), Coupon.class);
1867    }
1868
1869    /**
1870     * Delete a {@link Coupon}
1871     * <p>
1872     *
1873     * @param couponCode The code for the {@link Coupon}
1874     */
1875    public void deleteCoupon(final String couponCode) {
1876        doDELETE(Coupon.COUPON_RESOURCE + "/" + urlEncode(couponCode));
1877    }
1878
1879    /**
1880     * Restore a {@link Coupon} by the coupon code and potentially update its editable fields
1881     * <p>
1882     *
1883     * @param couponCode The coupon code to restore
1884     * @param coupon A {@link Coupon} containing fields to update
1885     * @return
1886     */
1887    public Coupon restoreCoupon(final String couponCode, final Coupon coupon) {
1888        return doPUT(Coupon.COUPON_RESOURCE + "/" + urlEncode(couponCode) + Coupon.RESTORE_RESOURCE,
1889                coupon, Coupon.class);
1890    }
1891
1892    ///////////////////////////////////////////////////////////////////////////
1893
1894    /**
1895     * Redeem a {@link Coupon} on an account.
1896     *
1897     * @param couponCode redeemed coupon id
1898     * @return the {@link Coupon} object
1899     */
1900    public Redemption redeemCoupon(final String couponCode, final Redemption redemption) {
1901        return doPOST(Coupon.COUPON_RESOURCE + "/" + urlEncode(couponCode) + Redemption.REDEEM_RESOURCE,
1902                      redemption, Redemption.class);
1903    }
1904
1905    /**
1906     * Lookup the first coupon redemption on an account.
1907     *
1908     * @param accountCode recurly account id
1909     * @return the coupon redemption for this account on success, null otherwise
1910     */
1911    public Redemption getCouponRedemptionByAccount(final String accountCode) {
1912        return doGET(Accounts.ACCOUNTS_RESOURCE + "/" + urlEncode(accountCode) + Redemption.REDEMPTION_RESOURCE,
1913                     Redemption.class);
1914    }
1915
1916    /**
1917     * Lookup all coupon redemptions on an account.
1918     *
1919     * @param accountCode recurly account id
1920     * @return the coupon redemptions for this account on success, null otherwise
1921     */
1922    public Redemptions getCouponRedemptionsByAccount(final String accountCode) {
1923        return doGET(Accounts.ACCOUNTS_RESOURCE + "/" + urlEncode(accountCode) + Redemption.REDEMPTIONS_RESOURCE,
1924                Redemptions.class, new QueryParams());
1925    }
1926
1927    /**
1928     * Lookup all coupon redemptions on an account given query params.
1929     *
1930     * @param accountCode recurly account id
1931     * @param params {@link QueryParams}
1932     * @return the coupon redemptions for this account on success, null otherwise
1933     */
1934    public Redemptions getCouponRedemptionsByAccount(final String accountCode, final QueryParams params) {
1935        return doGET(Accounts.ACCOUNTS_RESOURCE + "/" + urlEncode(accountCode) + Redemption.REDEMPTIONS_RESOURCE,
1936                Redemptions.class, params);
1937    }
1938
1939    /**
1940     * Lookup the first coupon redemption on an invoice.
1941     *
1942     * @deprecated Prefer using Invoice#getId() as the id param (which is a String)
1943     *
1944     * @param invoiceNumber invoice number
1945     * @return the coupon redemption for this invoice on success, null otherwise
1946     */
1947    @Deprecated
1948    public Redemption getCouponRedemptionByInvoice(final Integer invoiceNumber) {
1949        return getCouponRedemptionByInvoice(invoiceNumber.toString());
1950    }
1951
1952    /**
1953     * Lookup the first coupon redemption on an invoice.
1954     *
1955     * @param invoiceId String invoice id
1956     * @return the coupon redemption for this invoice on success, null otherwise
1957     */
1958    public Redemption getCouponRedemptionByInvoice(final String invoiceId) {
1959        return doGET(Invoices.INVOICES_RESOURCE + "/" + urlEncode(invoiceId) + Redemption.REDEMPTION_RESOURCE,
1960                Redemption.class);
1961    }
1962
1963
1964    /**
1965     * Lookup all coupon redemptions on an invoice.
1966     *
1967     * @deprecated Prefer using Invoice#getId() as the id param (which is a String)
1968     *
1969     * @param invoiceNumber invoice number
1970     * @return the coupon redemptions for this invoice on success, null otherwise
1971     */
1972    @Deprecated
1973    public Redemptions getCouponRedemptionsByInvoice(final Integer invoiceNumber) {
1974        return getCouponRedemptionsByInvoice(invoiceNumber.toString(), new QueryParams());
1975    }
1976
1977    /**
1978     * Lookup all coupon redemptions on an invoice.
1979     *
1980     * @param invoiceId String invoice id
1981     * @return the coupon redemptions for this invoice on success, null otherwise
1982     */
1983    public Redemptions getCouponRedemptionsByInvoice(final String invoiceId) {
1984        return getCouponRedemptionsByInvoice(invoiceId, new QueryParams());
1985    }
1986
1987    /**
1988     * Lookup all coupon redemptions on an invoice given query params.
1989     *
1990     * @deprecated Prefer using Invoice#getId() as the id param (which is a String)
1991     *
1992     * @param invoiceNumber invoice number
1993     * @param params {@link QueryParams}
1994     * @return the coupon redemptions for this invoice on success, null otherwise
1995     */
1996    @Deprecated
1997    public Redemptions getCouponRedemptionsByInvoice(final Integer invoiceNumber, final QueryParams params) {
1998        return getCouponRedemptionsByInvoice(invoiceNumber.toString(), params);
1999    }
2000
2001    /**
2002     * Lookup all coupon redemptions on an invoice given query params.
2003     *
2004     * @param invoiceId String invoice id
2005     * @param params {@link QueryParams}
2006     * @return the coupon redemptions for this invoice on success, null otherwise
2007     */
2008    public Redemptions getCouponRedemptionsByInvoice(final String invoiceId, final QueryParams params) {
2009        return doGET(Invoices.INVOICES_RESOURCE + "/" + urlEncode(invoiceId) + Redemption.REDEMPTIONS_RESOURCE,
2010                Redemptions.class, params);
2011    }
2012
2013    /**
2014     * Lookup all coupon redemptions on a subscription given query params.
2015     *
2016     * @param subscriptionUuid String subscription uuid
2017     * @param params {@link QueryParams}
2018     * @return the coupon redemptions for this subscription on success, null otherwise
2019     */
2020    public Redemptions getCouponRedemptionsBySubscription(final String subscriptionUuid, final QueryParams params) {
2021        return doGET(Subscription.SUBSCRIPTION_RESOURCE + "/" + urlEncode(subscriptionUuid) + Redemptions.REDEMPTIONS_RESOURCE,
2022                Redemptions.class, params);
2023    }
2024
2025    /**
2026     * Deletes a coupon redemption from an account.
2027     *
2028     * @param accountCode recurly account id
2029     */
2030    public void deleteCouponRedemption(final String accountCode) {
2031        doDELETE(Accounts.ACCOUNTS_RESOURCE + "/" + urlEncode(accountCode) + Redemption.REDEMPTION_RESOURCE);
2032    }
2033
2034    /**
2035     * Deletes a specific redemption.
2036     *
2037     * @param accountCode recurly account id
2038     * @param redemptionUuid recurly coupon redemption uuid
2039     */
2040    public void deleteCouponRedemption(final String accountCode, final String redemptionUuid) {
2041        doDELETE(Accounts.ACCOUNTS_RESOURCE + "/" + urlEncode(accountCode) + Redemption.REDEMPTIONS_RESOURCE + "/" + urlEncode(redemptionUuid));
2042    }
2043
2044    /**
2045     * Generates unique codes for a bulk coupon.
2046     *
2047     * @param couponCode recurly coupon code (must have been created as type: bulk)
2048     * @param coupon A coupon with number of unique codes set
2049     */
2050    public Coupons generateUniqueCodes(final String couponCode, final Coupon coupon) {
2051        Coupons coupons = doPOST(Coupon.COUPON_RESOURCE + "/" + urlEncode(couponCode) + Coupon.GENERATE_RESOURCE, coupon, Coupons.class);
2052        return coupons.getStart();
2053    }
2054
2055    /**
2056     * Lookup all unique codes for a bulk coupon given query params.
2057     *
2058     * @param couponCode String coupon code
2059     * @param params {@link QueryParams}
2060     * @return the unique coupon codes for the coupon code on success, null otherwise
2061     */
2062    public Coupons getUniqueCouponCodes(final String couponCode, final QueryParams params) {
2063        return doGET(Coupon.COUPON_RESOURCE + "/" + urlEncode(couponCode) + Coupon.UNIQUE_CODES_RESOURCE,
2064                Coupons.class, params);
2065    }
2066
2067    ///////////////////////////////////////////////////////////////////////////
2068    //
2069    // Recurly.js API
2070    //
2071    ///////////////////////////////////////////////////////////////////////////
2072
2073    /**
2074     * Fetch Subscription
2075     * <p>
2076     * Returns subscription from a recurly.js token.
2077     *
2078     * @param recurlyToken token given by recurly.js
2079     * @return subscription object on success, null otherwise
2080     */
2081    public Subscription fetchSubscription(final String recurlyToken) {
2082        return fetch(recurlyToken, Subscription.class);
2083    }
2084
2085    /**
2086     * Fetch BillingInfo
2087     * <p>
2088     * Returns billing info from a recurly.js token.
2089     *
2090     * @param recurlyToken token given by recurly.js
2091     * @return billing info object on success, null otherwise
2092     */
2093    public BillingInfo fetchBillingInfo(final String recurlyToken) {
2094        return fetch(recurlyToken, BillingInfo.class);
2095    }
2096
2097    /**
2098     * Fetch Invoice
2099     * <p>
2100     * Returns invoice from a recurly.js token.
2101     *
2102     * @param recurlyToken token given by recurly.js
2103     * @return invoice object on success, null otherwise
2104     */
2105    public Invoice fetchInvoice(final String recurlyToken) {
2106        return fetch(recurlyToken, Invoice.class);
2107    }
2108
2109    /**
2110     * Get Gift Cards given query params
2111     * <p>
2112     * Returns information about all gift cards.
2113     *
2114     * @param params {@link QueryParams}
2115     * @return gitfcards object on success, null otherwise
2116     */
2117    public GiftCards getGiftCards(final QueryParams params) {
2118        return doGET(GiftCards.GIFT_CARDS_RESOURCE, GiftCards.class, params);
2119    }
2120
2121    /**
2122     * Get Gift Cards
2123     * <p>
2124     * Returns information about all gift cards.
2125     *
2126     * @return gitfcards object on success, null otherwise
2127     */
2128    public GiftCards getGiftCards() {
2129        return doGET(GiftCards.GIFT_CARDS_RESOURCE, GiftCards.class, new QueryParams());
2130    }
2131
2132    /**
2133     * Get number of GiftCards matching the query params
2134     *
2135     * @param params {@link QueryParams}
2136     * @return Integer on success, null otherwise
2137     */
2138    public Integer getGiftCardsCount(final QueryParams params) {
2139        HeaderGroup map = doHEAD(GiftCards.GIFT_CARDS_RESOURCE, params);
2140        return Integer.parseInt(map.getFirstHeader(X_RECORDS_HEADER_NAME).getValue());
2141    }
2142
2143    /**
2144     * Get a Gift Card
2145     * <p>
2146     *
2147     * @param giftCardId The id for the {@link GiftCard}
2148     * @return The {@link GiftCard} object as identified by the passed in id
2149     */
2150    public GiftCard getGiftCard(final Long giftCardId) {
2151        return doGET(GiftCards.GIFT_CARDS_RESOURCE + "/" + Long.toString(giftCardId), GiftCard.class);
2152    }
2153
2154    /**
2155     * Redeem a Gift Card
2156     * <p>
2157     *
2158     * @param redemptionCode The redemption code the {@link GiftCard}
2159     * @param accountCode The account code for the {@link Account}
2160     * @return The updated {@link GiftCard} object as identified by the passed in id
2161     */
2162    public GiftCard redeemGiftCard(final String redemptionCode, final String accountCode) {
2163        final GiftCard.Redemption redemptionData = GiftCard.createRedemption(accountCode);
2164        final String url = GiftCards.GIFT_CARDS_RESOURCE + "/" + urlEncode(redemptionCode) + "/redeem";
2165
2166        return doPOST(url, redemptionData, GiftCard.class);
2167    }
2168
2169    /**
2170     * Purchase a GiftCard
2171     * <p>
2172     *
2173     * @param giftCard The giftCard data
2174     * @return the giftCard object
2175     */
2176    public GiftCard purchaseGiftCard(final GiftCard giftCard) {
2177        return doPOST(GiftCards.GIFT_CARDS_RESOURCE, giftCard, GiftCard.class);
2178    }
2179
2180    /**
2181     * Preview a GiftCard
2182     * <p>
2183     *
2184     * @param giftCard The giftCard data
2185     * @return the giftCard object
2186     */
2187    public GiftCard previewGiftCard(final GiftCard giftCard) {
2188        return doPOST(GiftCards.GIFT_CARDS_RESOURCE + "/preview", giftCard, GiftCard.class);
2189    }
2190
2191    /**
2192     * Return all the MeasuredUnits
2193     * <p>
2194     *
2195     * @return the MeasuredUnits object as identified by the passed in ID
2196     */
2197    public MeasuredUnits getMeasuredUnits() {
2198        return doGET(MeasuredUnits.MEASURED_UNITS_RESOURCE, MeasuredUnits.class, new QueryParams());
2199    }
2200
2201    /**
2202     * Create a MeasuredUnit's info
2203     * <p>
2204     *
2205     * @param measuredUnit The measuredUnit to create on recurly
2206     * @return the measuredUnit object as identified by the passed in ID
2207     */
2208    public MeasuredUnit createMeasuredUnit(final MeasuredUnit measuredUnit) {
2209        return doPOST(MeasuredUnit.MEASURED_UNITS_RESOURCE, measuredUnit, MeasuredUnit.class);
2210    }
2211
2212    /**
2213     * Purchases endpoint
2214     * <p>
2215     * https://developers.recurly.com/api-v2/v2.29/index.html#operation/createPurchase
2216     *
2217     * @param purchase The purchase data
2218     * @return The created invoice collection
2219     */
2220    public InvoiceCollection purchase(final Purchase purchase) {
2221        return doPOST(Purchase.PURCHASES_ENDPOINT, purchase, InvoiceCollection.class);
2222    }
2223
2224    /**
2225     * Purchases preview endpoint
2226     * <p>
2227     * https://developers.recurly.com/api-v2/v2.29/index.html#operation/previewPurchase
2228     *
2229     * @param purchase The purchase data
2230     * @return The preview invoice collection
2231     */
2232    public InvoiceCollection previewPurchase(final Purchase purchase) {
2233        return doPOST(Purchase.PURCHASES_ENDPOINT + "/preview", purchase, InvoiceCollection.class);
2234    }
2235
2236    /**
2237     * Purchases authorize endpoint.
2238     *
2239     * Generate an authorized invoice for the purchase. Runs validations
2240     + but does not run any transactions. This endpoint will create a
2241     + pending purchase that can be activated at a later time once payment
2242     + has been completed on an external source (e.g. Adyen's Hosted
2243     + Payment Pages).
2244     *
2245     * <p>
2246     * https://developers.recurly.com/api-v2/v2.29/index.html#operation/authorizePurchase
2247     *
2248     * @param purchase The purchase data
2249     * @return The authorized invoice collection
2250     */
2251    public InvoiceCollection authorizePurchase(final Purchase purchase) {
2252        return doPOST(Purchase.PURCHASES_ENDPOINT + "/authorize", purchase, InvoiceCollection.class);
2253    }
2254
2255    /**
2256     * Purchases pending endpoint.
2257     *
2258     * Use for Adyen HPP transaction requests. Runs validations
2259     + but does not run any transactions.
2260     *
2261     * <p>
2262     * https://developers.recurly.com/api-v2/v2.29/index.html#operation/pendingPurchase
2263     *
2264     * @param purchase The purchase data
2265     * @return The authorized invoice collection
2266     */
2267    public InvoiceCollection pendingPurchase(final Purchase purchase) {
2268        return doPOST(Purchase.PURCHASES_ENDPOINT + "/pending", purchase, InvoiceCollection.class);
2269    }
2270
2271    /**
2272     * Purchases capture endpoint.
2273     * 
2274     * Capture an open Authorization request
2275     * 
2276     * <p>
2277     * https://developers.recurly.com/api-v2/v2.29/index.html#operation/capturePurchase
2278     * 
2279     * @param transactionUuid UUID of the transaction to cancel
2280     */
2281    public InvoiceCollection capturePurchase(final String transactionUuid) {
2282        return doPOST(Purchase.PURCHASES_ENDPOINT + "/transaction-uuid-" + transactionUuid + "/capture", null, InvoiceCollection.class);
2283    }
2284
2285    /**
2286     * Purchases cancel endpoint.
2287     * 
2288     * Cancel an open Authorization request
2289     * 
2290     * <p>
2291     * https://developers.recurly.com/api-v2/v2.29/index.html#operation/cancelPurchase
2292     * 
2293     * @param transactionUuid UUID of the transaction to capture
2294     */
2295    public InvoiceCollection cancelPurchase(final String transactionUuid) {
2296        return doPOST(Purchase.PURCHASES_ENDPOINT + "/transaction-uuid-" + transactionUuid + "/cancel", null, InvoiceCollection.class);
2297    }
2298
2299    /**
2300     * Sets the acquisition details for an account
2301     * <p>
2302     * https://developers.recurly.com/api-v2/v2.29/index.html#operation/createAccountAcquisition
2303     *
2304     * @param accountCode The account's account code
2305     * @param acquisition The AccountAcquisition data
2306     * @return The created AccountAcquisition object
2307     */
2308    public AccountAcquisition createAccountAcquisition(final String accountCode, final AccountAcquisition acquisition) {
2309        final String path = Account.ACCOUNT_RESOURCE + "/" + urlEncode(accountCode) + AccountAcquisition.ACCOUNT_ACQUISITION_RESOURCE;
2310        return doPOST(path, acquisition, AccountAcquisition.class);
2311    }
2312
2313    /**
2314     * Gets the acquisition details for an account
2315     * <p>
2316     * https://developers.recurly.com/api-v2/v2.29/index.html#operation/lookupAccountAcquisition
2317     *
2318     * @param accountCode The account's account code
2319     * @return The created AccountAcquisition object
2320     */
2321    public AccountAcquisition getAccountAcquisition(final String accountCode) {
2322        final String path = Account.ACCOUNT_RESOURCE + "/" + urlEncode(accountCode) + AccountAcquisition.ACCOUNT_ACQUISITION_RESOURCE;
2323        return doGET(path, AccountAcquisition.class);
2324    }
2325
2326    /**
2327     * Updates the acquisition details for an account
2328     * <p>
2329     * https://developers.recurly.com/api-v2/v2.29/index.html#operation/updateAccountAcquisition
2330     *
2331     * @param accountCode The account's account code
2332     * @param acquisition The AccountAcquisition data
2333     * @return The created AccountAcquisition object
2334     */
2335    public AccountAcquisition updateAccountAcquisition(final String accountCode, final AccountAcquisition acquisition) {
2336        final String path = Account.ACCOUNT_RESOURCE + "/" + urlEncode(accountCode) + AccountAcquisition.ACCOUNT_ACQUISITION_RESOURCE;
2337        return doPUT(path, acquisition, AccountAcquisition.class);
2338    }
2339
2340    /**
2341     * Clear the acquisition details for an account
2342     * <p>
2343     * https://developers.recurly.com/api-v2/v2.29/index.html#operation/clearAccountAcquisition
2344     *
2345     * @param accountCode The account's account code
2346     */
2347    public void deleteAccountAcquisition(final String accountCode) {
2348        doDELETE(Account.ACCOUNT_RESOURCE + "/" + urlEncode(accountCode) + AccountAcquisition.ACCOUNT_ACQUISITION_RESOURCE);
2349    }
2350
2351
2352    /**
2353     * Get Credit Payments
2354     * <p>
2355     * Returns information about all credit payments.
2356     *
2357     * @return CreditPayments on success, null otherwise
2358     */
2359    public CreditPayments getCreditPayments() {
2360        return doGET(CreditPayments.CREDIT_PAYMENTS_RESOURCE, CreditPayments.class, new QueryParams());
2361    }
2362
2363    /**
2364     * Get Credit Payments
2365     * <p>
2366     * Returns information about all credit payments.
2367     *
2368     * @param params {@link QueryParams}
2369     * @return CreditPayments on success, null otherwise
2370     */
2371    public CreditPayments getCreditPayments(final QueryParams params) {
2372        return doGET(CreditPayments.CREDIT_PAYMENTS_RESOURCE, CreditPayments.class, params);
2373    }
2374
2375    /**
2376     * Get Credit Payments for a given account
2377     * <p>
2378     * Returns information about all credit payments.
2379     *
2380     * @param accountCode The account code to filter
2381     * @param params {@link QueryParams}
2382     * @return CreditPayments on success, null otherwise
2383     */
2384    public CreditPayments getCreditPayments(final String accountCode, final QueryParams params) {
2385        final String path = Accounts.ACCOUNTS_RESOURCE + "/" + urlEncode(accountCode) + CreditPayments.CREDIT_PAYMENTS_RESOURCE;
2386        return doGET(path, CreditPayments.class, params);
2387    }
2388
2389    /**
2390     * Get Shipping Methods for the site
2391     * <p>
2392     * https://developers.recurly.com/api-v2/v2.29/index.html#operation/listShippingMethods
2393     *
2394     * @return ShippingMethods on success, null otherwise
2395     */
2396    public ShippingMethods getShippingMethods() {
2397        return doGET(ShippingMethods.SHIPPING_METHODS_RESOURCE, ShippingMethods.class, new QueryParams());
2398    }
2399
2400    /**
2401     * Get Shipping Methods for the site
2402     * <p>
2403     * https://developers.recurly.com/api-v2/v2.29/index.html#operation/listShippingMethods
2404     *
2405     * @param params {@link QueryParams}
2406     * @return ShippingMethods on success, null otherwise
2407     */
2408    public ShippingMethods getShippingMethods(final QueryParams params) {
2409        return doGET(ShippingMethods.SHIPPING_METHODS_RESOURCE, ShippingMethods.class, params);
2410    }
2411
2412    /**
2413     * Look up a shipping method
2414     * <p>
2415     * https://developers.recurly.com/api-v2/v2.29/index.html#operation/lookupShippingMethod
2416     *
2417     * @param shippingMethodCode The code for the {@link ShippingMethod}
2418     * @return The {@link ShippingMethod} object as identified by the passed in code
2419     */
2420    public ShippingMethod getShippingMethod(final String shippingMethodCode) {
2421        if (shippingMethodCode == null || shippingMethodCode.isEmpty())
2422            throw new RuntimeException("shippingMethodCode cannot be empty!");
2423
2424        return doGET(ShippingMethod.SHIPPING_METHOD_RESOURCE + "/" + urlEncode(shippingMethodCode), ShippingMethod.class);
2425    }
2426
2427    private <T> T fetch(final String recurlyToken, final Class<T> clazz) {
2428        return doGET(FETCH_RESOURCE + "/" + urlEncode(recurlyToken), clazz);
2429    }
2430
2431    ///////////////////////////////////////////////////////////////////////////
2432
2433    private InputStream doGETPdf(final String resource) {
2434        return doGETPdfWithFullURL(baseUrl + resource);
2435    }
2436
2437    private <T> T doGET(final String resource, final Class<T> clazz) {
2438        return doGETWithFullURL(clazz, baseUrl + resource);
2439    }
2440
2441    private <T> T doGET(final String resource, final Class<T> clazz, QueryParams params) {
2442        return doGETWithFullURL(clazz, constructUrl(resource, params));
2443    }
2444
2445    private String constructUrl(final String resource, QueryParams params) {
2446        return baseUrl + resource + params.toString();
2447    }
2448
2449    public <T> T doGETWithFullURL(final Class<T> clazz, final String url) {
2450        if (debug()) {
2451            log.info("Msg to Recurly API [GET] :: URL : {}", url);
2452        }
2453        return callRecurlySafeXmlContent(new HttpGet(url), clazz);
2454    }
2455
2456    private InputStream doGETPdfWithFullURL(final String url) {
2457        if (debug()) {
2458            log.info(" [GET] :: URL : {}", url);
2459        }
2460
2461        return callRecurlySafeGetPdf(url);
2462    }
2463
2464    private InputStream callRecurlySafeGetPdf(String url) {
2465        CloseableHttpResponse response = null;
2466        InputStream pdfInputStream = null;
2467        try {
2468            final HttpGet builder = new HttpGet(url);
2469            clientRequestBuilderCommon(builder);
2470            builder.setHeader(HttpHeaders.ACCEPT, "application/pdf");
2471            builder.setHeader(HttpHeaders.CONTENT_TYPE, "application/pdf");
2472            response = client.execute(builder);
2473            if (response.getStatusLine().getStatusCode() != 200) {
2474                final RecurlyAPIError recurlyAPIError = RecurlyAPIError.buildFromResponse(response);
2475                throw new RecurlyAPIException(recurlyAPIError);
2476            }
2477
2478            // Buffer the pdf in memory on purpose, because this was actually the behavior of AsyncHttpClient.
2479            final HttpEntity entity = response.getEntity();
2480            if (entity != null) {
2481                final byte[] pdfBytes = EntityUtils.toByteArray(entity);
2482                pdfInputStream = new ByteArrayInputStream(pdfBytes);
2483            }
2484        } catch (IOException e) {
2485            log.error("Error retrieving response body", e);
2486            return null;
2487        } finally {
2488            closeResponse(response);
2489        }
2490
2491        return pdfInputStream;
2492    }
2493
2494    private <T> T doPOST(final String resource, final RecurlyObject payload, final Class<T> clazz) {
2495        final String xmlPayload;
2496        try {
2497            if (payload != null) {
2498                xmlPayload = RecurlyObject.sharedXmlMapper().writeValueAsString(payload);
2499            } else {
2500                xmlPayload = null;
2501            }
2502            if (debug()) {
2503                log.info("Msg to Recurly API [POST]:: URL : {}", baseUrl + resource);
2504                log.info("Payload for [POST]:: {}", xmlPayload);
2505            }
2506        } catch (IOException e) {
2507            log.warn("Unable to serialize {} object as XML: {}", clazz.getName(), payload.toString());
2508            return null;
2509        }
2510
2511        final HttpPost builder = new HttpPost(baseUrl + resource);
2512        if (xmlPayload != null) {
2513            builder.setEntity(new StringEntity(xmlPayload,
2514                    ContentType.APPLICATION_XML.withCharset(Charsets.UTF_8)));
2515        }
2516        return callRecurlySafeXmlContent(builder, clazz);
2517    }
2518
2519    private <T> T doPUT(final String resource, final RecurlyObject payload, final Class<T> clazz) {
2520        return doPUT(resource, payload, clazz, new QueryParams());
2521    }
2522
2523    private <T> T doPUT(final String resource, final RecurlyObject payload, final Class<T> clazz, final QueryParams params) {
2524        final String xmlPayload;
2525        try {
2526            if (payload != null) {
2527                xmlPayload = RecurlyObject.sharedXmlMapper().writeValueAsString(payload);
2528            } else {
2529                xmlPayload = null;
2530            }
2531
2532            if (debug()) {
2533                log.info("Msg to Recurly API [PUT]:: URL : {}", baseUrl + resource);
2534                log.info("Payload for [PUT]:: {}", xmlPayload);
2535            }
2536        } catch (IOException e) {
2537            log.warn("Unable to serialize {} object as XML: {}", clazz.getName(), payload.toString());
2538            return null;
2539        }
2540
2541        final HttpPut builder = new HttpPut(constructUrl(resource, params));
2542        if (xmlPayload != null) {
2543            builder.setEntity(new StringEntity(xmlPayload,
2544                    ContentType.APPLICATION_XML.withCharset(Charsets.UTF_8)));
2545        }
2546        return callRecurlySafeXmlContent(builder, clazz);
2547    }
2548
2549    private HeaderGroup doHEAD(final String resource, QueryParams params) {
2550        if (params == null) {
2551            params = new QueryParams();
2552        }
2553
2554        final String url = constructUrl(resource, params);
2555        if (debug()) {
2556            log.info("Msg to Recurly API [HEAD]:: URL : {}", url);
2557        }
2558
2559        return callRecurlyNoContent(new HttpHead(url));
2560    }
2561
2562    private void doDELETE(final String resource) {
2563        callRecurlySafeXmlContent(new HttpDelete(baseUrl + resource), null);
2564    }
2565
2566    private HeaderGroup callRecurlyNoContent(final HttpRequestBase builder) {
2567        clientRequestBuilderCommon(builder);
2568        builder.setHeader(HttpHeaders.ACCEPT, "application/xml");
2569        builder.setHeader(HttpHeaders.CONTENT_TYPE, "application/xml; charset=utf-8");
2570        CloseableHttpResponse response = null;
2571        try {
2572            response = client.execute(builder);
2573            // Copy all the headers into a HeaderGroup, which will handle case insensitive headers for us
2574            final HeaderGroup headerGroup = new HeaderGroup();
2575            for (Header header : response.getAllHeaders()) {
2576                headerGroup.addHeader(header);
2577            }
2578            return headerGroup;
2579        } catch (IOException e) {
2580            log.error("Execution error", e);
2581            return null;
2582        } finally {
2583            closeResponse(response);
2584        }
2585    }
2586
2587    private <T> T callRecurlySafeXmlContent(final HttpRequestBase builder, @Nullable final Class<T> clazz) {
2588        try {
2589            return callRecurlyXmlContent(builder, clazz);
2590        } catch (IOException e) {
2591            if (e instanceof ConnectException || e instanceof NoHttpResponseException
2592                    || e instanceof ConnectTimeoutException || e instanceof SSLException) {
2593                // See https://github.com/killbilling/recurly-java-library/issues/185
2594                throw new ConnectionErrorException(e);
2595            }
2596            log.warn("Error while calling Recurly", e);
2597            return null;
2598        }
2599        // No need to extract TransactionErrorException since it's already a RuntimeException
2600    }
2601
2602    private <T> T callRecurlyXmlContent(final HttpRequestBase builder, @Nullable final Class<T> clazz)
2603            throws IOException {
2604        clientRequestBuilderCommon(builder);
2605        builder.setHeader(HttpHeaders.ACCEPT, "application/xml");
2606        builder.setHeader(HttpHeaders.CONTENT_TYPE, "application/xml; charset=utf-8");
2607        CloseableHttpResponse response = null;
2608        try {
2609            response = client.execute(builder);
2610            final String payload = convertEntityToString(response.getEntity());
2611            if (debug()) {
2612                log.info("Msg from Recurly API :: {}", payload);
2613            }
2614
2615            // Handle errors payload
2616            if (response.getStatusLine().getStatusCode() >= 300) {
2617                log.warn("Recurly error whilst calling: {}\n{}", builder.getURI(), payload);
2618                log.warn("Error status code: {}\n", response.getStatusLine().getStatusCode());
2619                RecurlyAPIError recurlyError = RecurlyAPIError.buildFromResponse(response);
2620
2621                if (response.getStatusLine().getStatusCode() == 422) {
2622                    // 422 is returned for transaction errors (see https://developers.recurly.com/pages/api-v2/transaction-errors.html)
2623                    // as well as bad input payloads
2624                    final Errors errors;
2625                    try {
2626                        errors = RecurlyObject.sharedXmlMapper().readValue(payload, Errors.class);
2627                    } catch (Exception e) {
2628                        log.warn("Unable to extract error", e);
2629                        return null;
2630                    }
2631
2632                    // Sometimes a single `Error` response is returned rather than `Errors`.
2633                    // In this case, all fields will be null.
2634                    if (errors == null || (
2635                        errors.getRecurlyErrors() == null &&
2636                        errors.getTransaction() == null &&
2637                        errors.getTransactionError() == null
2638                    )) {
2639                        recurlyError = RecurlyAPIError.buildFromXml(RecurlyObject.sharedXmlMapper(), payload, response);
2640                        throw new RecurlyAPIException(recurlyError);
2641                    }
2642                    throw new TransactionErrorException(errors);
2643                } else if (response.getStatusLine().getStatusCode() == 401) {
2644                    recurlyError.setSymbol("unauthorized");
2645                    recurlyError.setDescription("We could not authenticate your request. Either your subdomain and private key are not set or incorrect");
2646
2647                    throw new RecurlyAPIException(recurlyError);
2648                } else {
2649                    try {
2650                        recurlyError = RecurlyAPIError.buildFromXml(RecurlyObject.sharedXmlMapper(), payload, response);
2651                    } catch (Exception e) {
2652                        log.debug("Unable to extract error", e);
2653                    }
2654
2655                    throw new RecurlyAPIException(recurlyError);
2656                }
2657            }
2658
2659            if (clazz == null) {
2660                return null;
2661            }
2662
2663            final Header locationHeader = response.getFirstHeader(HttpHeaders.LOCATION);
2664            final String location = locationHeader == null ? null : locationHeader.getValue();
2665            if (clazz == Coupons.class && location != null && !location.isEmpty()) {
2666                final RecurlyObjects recurlyObjects = new Coupons();
2667                recurlyObjects.setRecurlyClient(this);
2668                recurlyObjects.setStartUrl(location);
2669                return (T) recurlyObjects;
2670            }
2671
2672            final T obj = RecurlyObject.sharedXmlMapper().readValue(payload, clazz);
2673            if (obj instanceof RecurlyObject) {
2674                ((RecurlyObject) obj).setRecurlyClient(this);
2675            } else if (obj instanceof RecurlyObjects) {
2676                final RecurlyObjects recurlyObjects = (RecurlyObjects) obj;
2677                recurlyObjects.setRecurlyClient(this);
2678
2679                // Set the RecurlyClient on all objects for later use
2680                for (final Object object : recurlyObjects) {
2681                    ((RecurlyObject) object).setRecurlyClient(this);
2682                }
2683
2684                // Set links for pagination
2685                final Header linkHeader = response.getFirstHeader(LINK_HEADER_NAME);
2686                if (linkHeader != null) {
2687                    final String[] links = PaginationUtils.getLinks(linkHeader.getValue());
2688                    recurlyObjects.setStartUrl(links[0]);
2689                    recurlyObjects.setNextUrl(links[1]);
2690                }
2691            }
2692
2693            // Save value of rate limit remaining header
2694            Header rateLimitRemainingString = response.getFirstHeader(X_RATELIMIT_REMAINING_HEADER_NAME);
2695            if (rateLimitRemainingString != null)
2696                rateLimitRemaining = Integer.parseInt(rateLimitRemainingString.getValue());
2697
2698            return obj;
2699        } finally {
2700            closeResponse(response);
2701        }
2702    }
2703
2704    private void clientRequestBuilderCommon(HttpRequestBase requestBuilder) {
2705        validateHost(requestBuilder.getURI());
2706        requestBuilder.setHeader(HttpHeaders.AUTHORIZATION, "Basic " + key);
2707        requestBuilder.setHeader("X-Api-Version", RECURLY_API_VERSION);
2708        requestBuilder.setHeader(HttpHeaders.USER_AGENT, userAgent);
2709        requestBuilder.setHeader(HttpHeaders.ACCEPT_LANGUAGE, acceptLanguage);
2710    }
2711
2712    private String convertEntityToString(HttpEntity entity) {
2713        if (entity == null) {
2714            return "";
2715        }
2716        final String entityString;
2717        try {
2718            entityString = EntityUtils.toString(entity, Charsets.UTF_8);
2719        } catch (ParseException e) {
2720            return "";
2721        } catch (IOException e) {
2722            return "";
2723        }
2724        return entityString == null ? "" : entityString;
2725    }
2726
2727    private void closeResponse(final CloseableHttpResponse response) {
2728        if (response != null) {
2729            try {
2730                response.close();
2731            } catch (IOException e) {
2732                log.warn("Failed to close {}: {}", response.getClass().getSimpleName(), e.getLocalizedMessage());
2733            }
2734        }
2735    }
2736
2737    protected CloseableHttpClient createHttpClient() throws KeyManagementException, NoSuchAlgorithmException {
2738        // Don't limit the number of connections per host
2739        // See https://github.com/ning/async-http-client/issues/issue/28
2740        final HttpClientBuilder httpClientBuilder = HttpClients.custom()
2741                .disableCookieManagement() // We don't need cookies
2742                /*
2743                 * The following limits are not quite truly unlimited, but in practice they
2744                 * should be more than enough.
2745                 */
2746                .setMaxConnPerRoute(256) // default is 2
2747                .setMaxConnTotal(512) // default is 20
2748                // Use the default timeouts from AHC
2749                .setDefaultRequestConfig(RequestConfig.custom()
2750                        .setConnectTimeout(5000).setSocketTimeout(60000).build())
2751                .setSSLContext(SslUtils.getInstance().getSSLContext());
2752        return httpClientBuilder.build();
2753    }
2754
2755    private void validateHost(URI uri) {
2756        String host = uri.getHost();
2757
2758        // Remove the subdomain from the host
2759        host = host.substring(host.indexOf(".")+1);
2760
2761        if (!validHosts.contains(host)) {
2762            String exc = String.format(Locale.ROOT, "Attempted to make call to %s instead of Recurly", host);
2763            throw new RuntimeException(exc);
2764        }
2765    }
2766
2767    @VisibleForTesting
2768    String getUserAgent() {
2769        return userAgent;
2770    }
2771
2772    private static String buildUserAgent() {
2773        final String defaultVersion = "0.0.0";
2774        final String defaultJavaVersion = "0.0.0";
2775
2776        try {
2777            final Properties gitRepositoryState = new Properties();
2778            final URL resourceURL = MoreObjects.firstNonNull(
2779                    Thread.currentThread().getContextClassLoader(),
2780                    RecurlyClient.class.getClassLoader()).getResource(GIT_PROPERTIES_FILE);
2781
2782            Reader reader = null;
2783            try {
2784                reader = new InputStreamReader(resourceURL.openStream(), Charsets.UTF_8);
2785                gitRepositoryState.load(reader);
2786            } finally {
2787                if (reader != null) {
2788                    reader.close();
2789                }
2790            }
2791
2792            final String version = MoreObjects.firstNonNull(getVersionFromGitRepositoryState(gitRepositoryState), defaultVersion);
2793            final String javaVersion = MoreObjects.firstNonNull(StandardSystemProperty.JAVA_VERSION.value(), defaultJavaVersion);
2794            return String.format(Locale.ROOT, "KillBill/%s; %s", version, javaVersion);
2795        } catch (final Exception e) {
2796            return String.format(Locale.ROOT, "KillBill/%s; %s", defaultVersion, defaultJavaVersion);
2797        }
2798    }
2799
2800    @VisibleForTesting
2801    static String getVersionFromGitRepositoryState(final Properties gitRepositoryState) {
2802        final String gitDescribe = gitRepositoryState.getProperty(GIT_COMMIT_ID_DESCRIBE_SHORT);
2803        if (gitDescribe == null) {
2804            return null;
2805        }
2806        final Matcher matcher = TAG_FROM_GIT_DESCRIBE_PATTERN.matcher(gitDescribe);
2807        return matcher.find() ? matcher.group(1) : null;
2808    }
2809
2810    /**
2811     * RFC 3986 URL encoding. The vanilla {@link URLEncoder} does not work since
2812     * Recurly does not decode '+' back to ' '.
2813     */
2814    private static String urlEncode(String s) {
2815        return new String(URLCodec.encodeUrl(RFC_3986_SAFE_CHARS, s.getBytes(Charsets.UTF_8)),
2816                Charsets.UTF_8);
2817    }
2818
2819    /**
2820     * Class that holds the cached user agent. This class exists so
2821     * {@link RecurlyClient#buildUserAgent()} will only run when the first instance
2822     * of {@link RecurlyClient} is created.
2823     */
2824    private static class UserAgentHolder {
2825
2826        private static final String userAgent = buildUserAgent();
2827
2828    }
2829
2830}