Use payment success timestamp to calculate recurring donation badge expiration

This commit is contained in:
Katherine 2023-12-12 07:01:20 -08:00 committed by GitHub
parent 6c7a3df5ae
commit 3cbbf37468
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 30 additions and 11 deletions

View File

@ -367,6 +367,7 @@ badges:
'1': TEST '1': TEST
subscription: # configuration for Stripe subscriptions subscription: # configuration for Stripe subscriptions
badgeExpiration: P30D
badgeGracePeriod: P15D badgeGracePeriod: P15D
levels: levels:
500: 500:

View File

@ -20,13 +20,16 @@ import javax.validation.constraints.NotNull;
public class SubscriptionConfiguration { public class SubscriptionConfiguration {
private final Duration badgeGracePeriod; private final Duration badgeGracePeriod;
private final Duration badgeExpiration;
private final Map<Long, SubscriptionLevelConfiguration> levels; private final Map<Long, SubscriptionLevelConfiguration> levels;
@JsonCreator @JsonCreator
public SubscriptionConfiguration( public SubscriptionConfiguration(
@JsonProperty("badgeGracePeriod") @Valid Duration badgeGracePeriod, @JsonProperty("badgeGracePeriod") @Valid Duration badgeGracePeriod,
@JsonProperty("badgeExpiration") @Valid Duration badgeExpiration,
@JsonProperty("levels") @Valid Map<@NotNull @Min(1) Long, @NotNull @Valid SubscriptionLevelConfiguration> levels) { @JsonProperty("levels") @Valid Map<@NotNull @Min(1) Long, @NotNull @Valid SubscriptionLevelConfiguration> levels) {
this.badgeGracePeriod = badgeGracePeriod; this.badgeGracePeriod = badgeGracePeriod;
this.badgeExpiration = badgeExpiration;
this.levels = levels; this.levels = levels;
} }
@ -34,6 +37,11 @@ public class SubscriptionConfiguration {
return badgeGracePeriod; return badgeGracePeriod;
} }
// This is the badge expiration time starting from when a payment successfully completes
public Duration getBadgeExpiration() {
return badgeExpiration;
}
public Map<Long, SubscriptionLevelConfiguration> getLevels() { public Map<Long, SubscriptionLevelConfiguration> getLevels() {
return levels; return levels;
} }

View File

@ -963,7 +963,7 @@ public class SubscriptionController {
try { try {
receiptCredentialResponse = zkReceiptOperations.issueReceiptCredential( receiptCredentialResponse = zkReceiptOperations.issueReceiptCredential(
receiptCredentialRequest, receiptCredentialRequest,
receiptExpirationWithGracePeriod(receipt.expiration()).getEpochSecond(), receipt.level()); receiptExpirationWithGracePeriod(receipt.paidAt()).getEpochSecond(), receipt.level());
} catch (VerificationFailedException e) { } catch (VerificationFailedException e) {
throw new BadRequestException("receipt credential request failed verification", e); throw new BadRequestException("receipt credential request failed verification", e);
} }
@ -1006,10 +1006,11 @@ public class SubscriptionController {
new ClientErrorException(Status.CONFLICT))) new ClientErrorException(Status.CONFLICT)))
.thenApply(customer -> Response.ok().build()); .thenApply(customer -> Response.ok().build());
} }
private Instant receiptExpirationWithGracePeriod(Instant itemExpiration) { private Instant receiptExpirationWithGracePeriod(Instant paidAt) {
return itemExpiration.plus(subscriptionConfiguration.getBadgeGracePeriod()) return paidAt.plus(subscriptionConfiguration.getBadgeExpiration())
.truncatedTo(ChronoUnit.DAYS) .plus(subscriptionConfiguration.getBadgeGracePeriod())
.plus(1, ChronoUnit.DAYS); .truncatedTo(ChronoUnit.DAYS)
.plus(1, ChronoUnit.DAYS);
} }
private String getSubscriptionTemplateId(long level, String currency, SubscriptionProcessor processor) { private String getSubscriptionTemplateId(long level, String currency, SubscriptionProcessor processor) {

View File

@ -542,7 +542,7 @@ public class BraintreeManager implements SubscriptionProcessorManager {
.build()); .build());
} }
final Instant expiration = transaction.getSubscriptionDetails().getBillingPeriodEndDate().toInstant(); final Instant paidAt = transaction.getSubscriptionDetails().getBillingPeriodStartDate().toInstant();
final Plan plan = braintreeGateway.plan().find(transaction.getPlanId()); final Plan plan = braintreeGateway.plan().find(transaction.getPlanId());
final BraintreePlanMetadata metadata; final BraintreePlanMetadata metadata;
@ -553,7 +553,7 @@ public class BraintreeManager implements SubscriptionProcessorManager {
throw new RuntimeException(e); 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))); .orElseThrow(() -> new WebApplicationException(Response.Status.NO_CONTENT)));
} }

View File

@ -606,14 +606,22 @@ public class StripeManager implements SubscriptionProcessorManager {
} }
InvoiceLineItem subscriptionLineItem = subscriptionLineItems.stream().findAny().get(); InvoiceLineItem subscriptionLineItem = subscriptionLineItems.stream().findAny().get();
return getReceiptForSubscriptionInvoiceLineItem(subscriptionLineItem); return getReceiptForSubscription(subscriptionLineItem, latestSubscriptionInvoice);
}); });
} }
private CompletableFuture<ReceiptItem> getReceiptForSubscriptionInvoiceLineItem(InvoiceLineItem subscriptionLineItem) { private CompletableFuture<ReceiptItem> 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( return getProductForPrice(subscriptionLineItem.getPrice().getId()).thenApply(product -> new ReceiptItem(
subscriptionLineItem.getId(), subscriptionLineItem.getId(),
Instant.ofEpochSecond(subscriptionLineItem.getPeriod().getEnd()), paidAt,
getLevelForProduct(product))); getLevelForProduct(product)));
} }

View File

@ -154,7 +154,7 @@ public interface SubscriptionProcessorManager {
} }
record ReceiptItem(String itemId, Instant expiration, long level) { record ReceiptItem(String itemId, Instant paidAt, long level) {
} }

View File

@ -965,6 +965,7 @@ class SubscriptionControllerTest {
} }
private static final String SUBSCRIPTION_CONFIG_YAML = """ private static final String SUBSCRIPTION_CONFIG_YAML = """
badgeExpiration: P30D
badgeGracePeriod: P15D badgeGracePeriod: P15D
levels: levels:
5: 5: