diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/controllers/SubscriptionController.java b/service/src/main/java/org/whispersystems/textsecuregcm/controllers/SubscriptionController.java index a60375918..2062d8041 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/controllers/SubscriptionController.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/controllers/SubscriptionController.java @@ -21,7 +21,6 @@ import java.time.Instant; import java.time.temporal.ChronoUnit; import java.util.Base64; import java.util.Collection; -import java.util.Iterator; import java.util.List; import java.util.Locale; import java.util.Map; @@ -743,7 +742,7 @@ public class SubscriptionController { .thenApply(this::requireRecordFromGetResult) .thenCompose(record -> { if (record.subscriptionId == null) { - return CompletableFuture.completedFuture(Response.noContent().build()); + return CompletableFuture.completedFuture(Response.status(Status.NOT_FOUND).build()); } ReceiptCredentialRequest receiptCredentialRequest; try { @@ -751,29 +750,20 @@ public class SubscriptionController { } catch (InvalidInputException e) { throw new BadRequestException("invalid receipt credential request", e); } - return stripeManager.getPaidInvoicesForSubscription(record.subscriptionId, requestData.now) - .thenCompose(invoices -> checkNextInvoice(invoices.iterator(), record.subscriptionId)) - .thenCompose(receipt -> { - if (receipt == null) { - return CompletableFuture.completedFuture(null); - } - return issuedReceiptsManager.recordIssuance( - receipt.getInvoiceLineItemId(), receiptCredentialRequest, requestData.now) - .thenApply(unused -> receipt); - }) + return stripeManager.getLatestInvoiceForSubscription(record.subscriptionId) + .thenCompose(invoice -> convertInvoiceToReceipt(invoice, record.subscriptionId)) + .thenCompose(receipt -> issuedReceiptsManager.recordIssuance( + receipt.getInvoiceLineItemId(), receiptCredentialRequest, requestData.now) + .thenApply(unused -> receipt)) .thenApply(receipt -> { - if (receipt == null) { - return Response.noContent().build(); - } else { - ReceiptCredentialResponse receiptCredentialResponse; - try { - receiptCredentialResponse = zkReceiptOperations.issueReceiptCredential( - receiptCredentialRequest, receipt.getExpiration().getEpochSecond(), receipt.getLevel()); - } catch (VerificationFailedException e) { - throw new BadRequestException("receipt credential request failed verification", e); - } - return Response.ok(new GetReceiptCredentialsResponse(receiptCredentialResponse.serialize())).build(); + ReceiptCredentialResponse receiptCredentialResponse; + try { + receiptCredentialResponse = zkReceiptOperations.issueReceiptCredential( + receiptCredentialRequest, receipt.getExpiration().getEpochSecond(), receipt.getLevel()); + } catch (VerificationFailedException e) { + throw new BadRequestException("receipt credential request failed verification", e); } + return Response.ok(new GetReceiptCredentialsResponse(receiptCredentialResponse.serialize())).build(); }); }); } @@ -803,35 +793,46 @@ public class SubscriptionController { } } - private CompletableFuture checkNextInvoice(Iterator invoiceIterator, String subscriptionId) { - if (!invoiceIterator.hasNext()) { - return null; + private CompletableFuture convertInvoiceToReceipt(Invoice latestSubscriptionInvoice, String subscriptionId) { + if (latestSubscriptionInvoice == null) { + throw new WebApplicationException(Status.NO_CONTENT); + } + if (StringUtils.equalsIgnoreCase("open", latestSubscriptionInvoice.getStatus())) { + throw new WebApplicationException(Status.NO_CONTENT); + } + if (!StringUtils.equalsIgnoreCase("paid", latestSubscriptionInvoice.getStatus())) { + throw new WebApplicationException(Status.PAYMENT_REQUIRED); } - Invoice invoice = invoiceIterator.next(); - return stripeManager.getInvoiceLineItemsForInvoice(invoice).thenCompose(invoiceLineItems -> { + return stripeManager.getInvoiceLineItemsForInvoice(latestSubscriptionInvoice).thenCompose(invoiceLineItems -> { Collection subscriptionLineItems = invoiceLineItems.stream() .filter(invoiceLineItem -> Objects.equals("subscription", invoiceLineItem.getType())) .collect(Collectors.toList()); if (subscriptionLineItems.isEmpty()) { - return checkNextInvoice(invoiceIterator, subscriptionId); + throw new IllegalStateException("latest subscription invoice has no subscription line items; subscriptionId=" + + subscriptionId + "; invoiceId=" + latestSubscriptionInvoice.getId()); } if (subscriptionLineItems.size() > 1) { - throw new IllegalStateException("invoice has more than one subscription; subscriptionId=" + subscriptionId - + "; count=" + subscriptionLineItems.size()); + throw new IllegalStateException( + "latest subscription invoice has too many subscription line items; subscriptionId=" + subscriptionId + + "; invoiceId=" + latestSubscriptionInvoice.getId() + "; count=" + subscriptionLineItems.size()); } InvoiceLineItem subscriptionLineItem = subscriptionLineItems.stream().findAny().get(); - return stripeManager.getProductForPrice(subscriptionLineItem.getPrice().getId()).thenApply(product -> new Receipt( - Instant.ofEpochSecond(subscriptionLineItem.getPeriod().getEnd()) - .plus(subscriptionConfiguration.getBadgeGracePeriod()) - .truncatedTo(ChronoUnit.DAYS) - .plus(1, ChronoUnit.DAYS), - stripeManager.getLevelForProduct(product), - subscriptionLineItem.getId())); + return getReceiptForSubscriptionInvoiceLineItem(subscriptionLineItem); }); } + private CompletableFuture getReceiptForSubscriptionInvoiceLineItem(InvoiceLineItem subscriptionLineItem) { + return stripeManager.getProductForPrice(subscriptionLineItem.getPrice().getId()).thenApply(product -> new Receipt( + Instant.ofEpochSecond(subscriptionLineItem.getPeriod().getEnd()) + .plus(subscriptionConfiguration.getBadgeGracePeriod()) + .truncatedTo(ChronoUnit.DAYS) + .plus(1, ChronoUnit.DAYS), + stripeManager.getLevelForProduct(product), + subscriptionLineItem.getId())); + } + private SubscriptionManager.Record requireRecordFromGetResult(SubscriptionManager.GetResult getResult) { if (getResult == GetResult.PASSWORD_MISMATCH) { throw new ForbiddenException("subscriberId mismatch"); diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/stripe/StripeManager.java b/service/src/main/java/org/whispersystems/textsecuregcm/stripe/StripeManager.java index 0270455c3..36293d062 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/stripe/StripeManager.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/stripe/StripeManager.java @@ -29,6 +29,7 @@ import com.stripe.param.SetupIntentCreateParams; import com.stripe.param.SubscriptionCancelParams; import com.stripe.param.SubscriptionCreateParams; import com.stripe.param.SubscriptionListParams; +import com.stripe.param.SubscriptionRetrieveParams; import com.stripe.param.SubscriptionUpdateParams; import com.stripe.param.SubscriptionUpdateParams.BillingCycleAnchor; import com.stripe.param.SubscriptionUpdateParams.ProrationBehavior; @@ -345,6 +346,19 @@ public class StripeManager { }, executor); } + public CompletableFuture getLatestInvoiceForSubscription(String subscriptionId) { + return CompletableFuture.supplyAsync(() -> { + SubscriptionRetrieveParams params = SubscriptionRetrieveParams.builder() + .addExpand("latest_invoice") + .build(); + try { + return Subscription.retrieve(subscriptionId, params, commonOptions()).getLatestInvoiceObject(); + } catch (StripeException e) { + throw new CompletionException(e); + } + }, executor); + } + public CompletableFuture> getInvoiceLineItemsForInvoice(Invoice invoice) { return CompletableFuture.supplyAsync( () -> Lists.newArrayList(invoice.getLines().autoPagingIterable(null, commonOptions())), executor);