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