Use latest invoice on subscription to generate receipts

This commit is contained in:
Ehren Kret 2021-11-19 11:25:38 -06:00
parent 6547d5ebf3
commit 279b0a51d9
2 changed files with 53 additions and 38 deletions

View File

@ -21,7 +21,6 @@ import java.time.Instant;
import java.time.temporal.ChronoUnit; import java.time.temporal.ChronoUnit;
import java.util.Base64; import java.util.Base64;
import java.util.Collection; import java.util.Collection;
import java.util.Iterator;
import java.util.List; import java.util.List;
import java.util.Locale; import java.util.Locale;
import java.util.Map; import java.util.Map;
@ -743,7 +742,7 @@ public class SubscriptionController {
.thenApply(this::requireRecordFromGetResult) .thenApply(this::requireRecordFromGetResult)
.thenCompose(record -> { .thenCompose(record -> {
if (record.subscriptionId == null) { if (record.subscriptionId == null) {
return CompletableFuture.completedFuture(Response.noContent().build()); return CompletableFuture.completedFuture(Response.status(Status.NOT_FOUND).build());
} }
ReceiptCredentialRequest receiptCredentialRequest; ReceiptCredentialRequest receiptCredentialRequest;
try { try {
@ -751,29 +750,20 @@ public class SubscriptionController {
} catch (InvalidInputException e) { } catch (InvalidInputException e) {
throw new BadRequestException("invalid receipt credential request", e); throw new BadRequestException("invalid receipt credential request", e);
} }
return stripeManager.getPaidInvoicesForSubscription(record.subscriptionId, requestData.now) return stripeManager.getLatestInvoiceForSubscription(record.subscriptionId)
.thenCompose(invoices -> checkNextInvoice(invoices.iterator(), record.subscriptionId)) .thenCompose(invoice -> convertInvoiceToReceipt(invoice, record.subscriptionId))
.thenCompose(receipt -> { .thenCompose(receipt -> issuedReceiptsManager.recordIssuance(
if (receipt == null) { receipt.getInvoiceLineItemId(), receiptCredentialRequest, requestData.now)
return CompletableFuture.completedFuture(null); .thenApply(unused -> receipt))
}
return issuedReceiptsManager.recordIssuance(
receipt.getInvoiceLineItemId(), receiptCredentialRequest, requestData.now)
.thenApply(unused -> receipt);
})
.thenApply(receipt -> { .thenApply(receipt -> {
if (receipt == null) { ReceiptCredentialResponse receiptCredentialResponse;
return Response.noContent().build(); try {
} else { receiptCredentialResponse = zkReceiptOperations.issueReceiptCredential(
ReceiptCredentialResponse receiptCredentialResponse; receiptCredentialRequest, receipt.getExpiration().getEpochSecond(), receipt.getLevel());
try { } catch (VerificationFailedException e) {
receiptCredentialResponse = zkReceiptOperations.issueReceiptCredential( throw new BadRequestException("receipt credential request failed verification", e);
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();
} }
return Response.ok(new GetReceiptCredentialsResponse(receiptCredentialResponse.serialize())).build();
}); });
}); });
} }
@ -803,35 +793,46 @@ public class SubscriptionController {
} }
} }
private CompletableFuture<Receipt> checkNextInvoice(Iterator<Invoice> invoiceIterator, String subscriptionId) { private CompletableFuture<Receipt> convertInvoiceToReceipt(Invoice latestSubscriptionInvoice, String subscriptionId) {
if (!invoiceIterator.hasNext()) { if (latestSubscriptionInvoice == null) {
return 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(latestSubscriptionInvoice).thenCompose(invoiceLineItems -> {
return stripeManager.getInvoiceLineItemsForInvoice(invoice).thenCompose(invoiceLineItems -> {
Collection<InvoiceLineItem> subscriptionLineItems = invoiceLineItems.stream() Collection<InvoiceLineItem> subscriptionLineItems = invoiceLineItems.stream()
.filter(invoiceLineItem -> Objects.equals("subscription", invoiceLineItem.getType())) .filter(invoiceLineItem -> Objects.equals("subscription", invoiceLineItem.getType()))
.collect(Collectors.toList()); .collect(Collectors.toList());
if (subscriptionLineItems.isEmpty()) { 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) { if (subscriptionLineItems.size() > 1) {
throw new IllegalStateException("invoice has more than one subscription; subscriptionId=" + subscriptionId throw new IllegalStateException(
+ "; count=" + subscriptionLineItems.size()); "latest subscription invoice has too many subscription line items; subscriptionId=" + subscriptionId
+ "; invoiceId=" + latestSubscriptionInvoice.getId() + "; count=" + subscriptionLineItems.size());
} }
InvoiceLineItem subscriptionLineItem = subscriptionLineItems.stream().findAny().get(); InvoiceLineItem subscriptionLineItem = subscriptionLineItems.stream().findAny().get();
return stripeManager.getProductForPrice(subscriptionLineItem.getPrice().getId()).thenApply(product -> new Receipt( return getReceiptForSubscriptionInvoiceLineItem(subscriptionLineItem);
Instant.ofEpochSecond(subscriptionLineItem.getPeriod().getEnd())
.plus(subscriptionConfiguration.getBadgeGracePeriod())
.truncatedTo(ChronoUnit.DAYS)
.plus(1, ChronoUnit.DAYS),
stripeManager.getLevelForProduct(product),
subscriptionLineItem.getId()));
}); });
} }
private CompletableFuture<Receipt> 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) { private SubscriptionManager.Record requireRecordFromGetResult(SubscriptionManager.GetResult getResult) {
if (getResult == GetResult.PASSWORD_MISMATCH) { if (getResult == GetResult.PASSWORD_MISMATCH) {
throw new ForbiddenException("subscriberId mismatch"); throw new ForbiddenException("subscriberId mismatch");

View File

@ -29,6 +29,7 @@ import com.stripe.param.SetupIntentCreateParams;
import com.stripe.param.SubscriptionCancelParams; import com.stripe.param.SubscriptionCancelParams;
import com.stripe.param.SubscriptionCreateParams; import com.stripe.param.SubscriptionCreateParams;
import com.stripe.param.SubscriptionListParams; import com.stripe.param.SubscriptionListParams;
import com.stripe.param.SubscriptionRetrieveParams;
import com.stripe.param.SubscriptionUpdateParams; import com.stripe.param.SubscriptionUpdateParams;
import com.stripe.param.SubscriptionUpdateParams.BillingCycleAnchor; import com.stripe.param.SubscriptionUpdateParams.BillingCycleAnchor;
import com.stripe.param.SubscriptionUpdateParams.ProrationBehavior; import com.stripe.param.SubscriptionUpdateParams.ProrationBehavior;
@ -345,6 +346,19 @@ public class StripeManager {
}, executor); }, executor);
} }
public CompletableFuture<Invoice> 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<Collection<InvoiceLineItem>> getInvoiceLineItemsForInvoice(Invoice invoice) { public CompletableFuture<Collection<InvoiceLineItem>> getInvoiceLineItemsForInvoice(Invoice invoice) {
return CompletableFuture.supplyAsync( return CompletableFuture.supplyAsync(
() -> Lists.newArrayList(invoice.getLines().autoPagingIterable(null, commonOptions())), executor); () -> Lists.newArrayList(invoice.getLines().autoPagingIterable(null, commonOptions())), executor);