Use payment success timestamp to calculate recurring donation badge expiration
This commit is contained in:
parent
6c7a3df5ae
commit
3cbbf37468
|
@ -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:
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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)));
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -154,7 +154,7 @@ public interface SubscriptionProcessorManager {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
record ReceiptItem(String itemId, Instant expiration, long level) {
|
record ReceiptItem(String itemId, Instant paidAt, long level) {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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:
|
||||||
|
|
Loading…
Reference in New Issue