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