diff --git a/service/config/sample.yml b/service/config/sample.yml index 15aa59e03..f04b62525 100644 --- a/service/config/sample.yml +++ b/service/config/sample.yml @@ -367,6 +367,7 @@ badges: '1': TEST subscription: # configuration for Stripe subscriptions + badgeExpiration: P30D badgeGracePeriod: P15D levels: 500: diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/configuration/SubscriptionConfiguration.java b/service/src/main/java/org/whispersystems/textsecuregcm/configuration/SubscriptionConfiguration.java index a9115ab55..9d29c4540 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/configuration/SubscriptionConfiguration.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/configuration/SubscriptionConfiguration.java @@ -20,13 +20,16 @@ import javax.validation.constraints.NotNull; public class SubscriptionConfiguration { private final Duration badgeGracePeriod; + private final Duration badgeExpiration; private final Map levels; @JsonCreator public SubscriptionConfiguration( @JsonProperty("badgeGracePeriod") @Valid Duration badgeGracePeriod, + @JsonProperty("badgeExpiration") @Valid Duration badgeExpiration, @JsonProperty("levels") @Valid Map<@NotNull @Min(1) Long, @NotNull @Valid SubscriptionLevelConfiguration> levels) { this.badgeGracePeriod = badgeGracePeriod; + this.badgeExpiration = badgeExpiration; this.levels = levels; } @@ -34,6 +37,11 @@ public class SubscriptionConfiguration { return badgeGracePeriod; } + // This is the badge expiration time starting from when a payment successfully completes + public Duration getBadgeExpiration() { + return badgeExpiration; + } + public Map getLevels() { return levels; } 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 460831aa2..aeb270957 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/controllers/SubscriptionController.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/controllers/SubscriptionController.java @@ -963,7 +963,7 @@ public class SubscriptionController { try { receiptCredentialResponse = zkReceiptOperations.issueReceiptCredential( receiptCredentialRequest, - receiptExpirationWithGracePeriod(receipt.expiration()).getEpochSecond(), receipt.level()); + receiptExpirationWithGracePeriod(receipt.paidAt()).getEpochSecond(), receipt.level()); } catch (VerificationFailedException e) { throw new BadRequestException("receipt credential request failed verification", e); } @@ -1006,10 +1006,11 @@ public class SubscriptionController { new ClientErrorException(Status.CONFLICT))) .thenApply(customer -> Response.ok().build()); } - private Instant receiptExpirationWithGracePeriod(Instant itemExpiration) { - return itemExpiration.plus(subscriptionConfiguration.getBadgeGracePeriod()) - .truncatedTo(ChronoUnit.DAYS) - .plus(1, ChronoUnit.DAYS); + private Instant receiptExpirationWithGracePeriod(Instant paidAt) { + return paidAt.plus(subscriptionConfiguration.getBadgeExpiration()) + .plus(subscriptionConfiguration.getBadgeGracePeriod()) + .truncatedTo(ChronoUnit.DAYS) + .plus(1, ChronoUnit.DAYS); } private String getSubscriptionTemplateId(long level, String currency, SubscriptionProcessor processor) { diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/subscriptions/BraintreeManager.java b/service/src/main/java/org/whispersystems/textsecuregcm/subscriptions/BraintreeManager.java index cc7c210c9..c0a5ef221 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/subscriptions/BraintreeManager.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/subscriptions/BraintreeManager.java @@ -542,7 +542,7 @@ public class BraintreeManager implements SubscriptionProcessorManager { .build()); } - final Instant expiration = transaction.getSubscriptionDetails().getBillingPeriodEndDate().toInstant(); + final Instant paidAt = transaction.getSubscriptionDetails().getBillingPeriodStartDate().toInstant(); final Plan plan = braintreeGateway.plan().find(transaction.getPlanId()); final BraintreePlanMetadata metadata; @@ -553,7 +553,7 @@ public class BraintreeManager implements SubscriptionProcessorManager { throw new RuntimeException(e); } - return new ReceiptItem(transaction.getId(), expiration, metadata.level()); + return new ReceiptItem(transaction.getId(), paidAt, metadata.level()); }) .orElseThrow(() -> new WebApplicationException(Response.Status.NO_CONTENT))); } diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/subscriptions/StripeManager.java b/service/src/main/java/org/whispersystems/textsecuregcm/subscriptions/StripeManager.java index baaae8d4f..f13637001 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/subscriptions/StripeManager.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/subscriptions/StripeManager.java @@ -606,14 +606,22 @@ public class StripeManager implements SubscriptionProcessorManager { } InvoiceLineItem subscriptionLineItem = subscriptionLineItems.stream().findAny().get(); - return getReceiptForSubscriptionInvoiceLineItem(subscriptionLineItem); + return getReceiptForSubscription(subscriptionLineItem, latestSubscriptionInvoice); }); } - private CompletableFuture getReceiptForSubscriptionInvoiceLineItem(InvoiceLineItem subscriptionLineItem) { + private CompletableFuture getReceiptForSubscription(InvoiceLineItem subscriptionLineItem, + Invoice invoice) { + final Instant paidAt; + if (invoice.getStatusTransitions().getPaidAt() != null) { + paidAt = Instant.ofEpochSecond(invoice.getStatusTransitions().getPaidAt()); + } else { + logger.warn("No paidAt timestamp exists for paid invoice {}, falling back to end of subscription period", invoice.getId()); + paidAt = Instant.ofEpochSecond(subscriptionLineItem.getPeriod().getEnd()); + } return getProductForPrice(subscriptionLineItem.getPrice().getId()).thenApply(product -> new ReceiptItem( subscriptionLineItem.getId(), - Instant.ofEpochSecond(subscriptionLineItem.getPeriod().getEnd()), + paidAt, getLevelForProduct(product))); } diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/subscriptions/SubscriptionProcessorManager.java b/service/src/main/java/org/whispersystems/textsecuregcm/subscriptions/SubscriptionProcessorManager.java index 77e541e54..febd2858d 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/subscriptions/SubscriptionProcessorManager.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/subscriptions/SubscriptionProcessorManager.java @@ -154,7 +154,7 @@ public interface SubscriptionProcessorManager { } - record ReceiptItem(String itemId, Instant expiration, long level) { + record ReceiptItem(String itemId, Instant paidAt, long level) { } diff --git a/service/src/test/java/org/whispersystems/textsecuregcm/controllers/SubscriptionControllerTest.java b/service/src/test/java/org/whispersystems/textsecuregcm/controllers/SubscriptionControllerTest.java index c18b119f2..37aef1e5b 100644 --- a/service/src/test/java/org/whispersystems/textsecuregcm/controllers/SubscriptionControllerTest.java +++ b/service/src/test/java/org/whispersystems/textsecuregcm/controllers/SubscriptionControllerTest.java @@ -965,6 +965,7 @@ class SubscriptionControllerTest { } private static final String SUBSCRIPTION_CONFIG_YAML = """ + badgeExpiration: P30D badgeGracePeriod: P15D levels: 5: