package org.rakam.ui.user;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.google.common.base.Throwables;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.inject.Inject;
import com.stripe.exception.InvalidRequestException;
import com.stripe.exception.StripeException;
import com.stripe.model.Coupon;
import com.stripe.model.Customer;
import com.stripe.model.Plan;
import com.stripe.net.RequestOptions;
import io.netty.handler.codec.http.HttpResponseStatus;
import org.rakam.server.http.HttpService;
import org.rakam.server.http.annotations.ApiParam;
import org.rakam.server.http.annotations.HeaderParam;
import org.rakam.server.http.annotations.IgnoreApi;
import org.rakam.server.http.annotations.JsonRequest;
import org.rakam.ui.ProtectEndpoint;
import org.rakam.ui.RakamUIConfig;
import org.rakam.ui.UIPermissionParameterProvider;
import org.rakam.util.RakamException;
import javax.ws.rs.Path;
import java.time.Instant;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.stream.Collectors;
import static io.netty.handler.codec.http.HttpResponseStatus.BAD_REQUEST;
import static io.netty.handler.codec.http.HttpResponseStatus.FORBIDDEN;
import static io.netty.handler.codec.http.HttpResponseStatus.NOT_FOUND;
import static io.netty.handler.codec.http.HttpResponseStatus.UNAUTHORIZED;
// TODO: CSRF!
@Path("/ui/subscription")
@IgnoreApi
public class UserSubscriptionHttpService
extends HttpService
{
private final WebUserService service;
private final RequestOptions requestOptions;
@Inject
public UserSubscriptionHttpService(WebUserService service, RakamUIConfig config)
{
this.service = service;
requestOptions = new RequestOptions.RequestOptionsBuilder()
.setApiKey(config.getStripeKey()).build();
}
@JsonRequest
@ProtectEndpoint(writeOperation = true, requiresProject = false)
@Path("/plans")
public List<RakamPlan> listPlans(@javax.inject.Named("user_id") UIPermissionParameterProvider.Project project)
{
Optional<WebUser> webUser = service.getUser(project.userId);
if (!webUser.isPresent()) {
throw new RakamException(FORBIDDEN);
}
try {
return Plan.list(ImmutableMap.of("limit", 50), requestOptions).getData().stream()
.map(e -> new RakamPlan(e.getId(), e.getName(), e.getAmount(), e.getStatementDescriptor()))
.collect(Collectors.toList());
}
catch (Exception e) {
throw Throwables.propagate(e);
}
}
@JsonRequest
@ProtectEndpoint(writeOperation = true, requiresProject = false)
@Path("/create")
public List<UserSubscription> create(
@ApiParam("token") String token,
@ApiParam(value = "coupon", required = false) String coupon,
@ApiParam("plan") String plan,
@javax.inject.Named("user_id") UIPermissionParameterProvider.Project project,
@HeaderParam("X-Requested-With") String csrfHeader)
{
if (!"XMLHttpRequest".equals(csrfHeader)) {
throw new RakamException(FORBIDDEN);
}
Optional<WebUser> webUser = service.getUser(project.userId);
if (!webUser.isPresent() || webUser.get().readOnly) {
throw new RakamException("User is not allowed to perform this operation", UNAUTHORIZED);
}
String userStripeId = service.getUserStripeId(project.userId);
Customer customer;
try {
Map<String, Object> customerParams = new HashMap<>();
customerParams.put("email", webUser.get().email);
customerParams.put("source", token);
if (userStripeId != null) {
customer = Customer.retrieve(userStripeId, requestOptions);
if (customer.getDeleted() == Boolean.TRUE) {
customer = Customer.create(customerParams, requestOptions);
service.setStripeId(webUser.get().id, customer.getId());
}
else {
customer.update(customerParams, requestOptions);
}
}
else {
customer = Customer.create(customerParams, requestOptions);
service.setStripeId(webUser.get().id, customer.getId());
}
if (plan != null && !plan.isEmpty()) {
Map<String, Object> subsParams = new HashMap<>();
subsParams.put("plan", plan);
if (coupon != null && !coupon.isEmpty()) {
subsParams.put("coupon", coupon);
}
customer.createSubscription(subsParams, requestOptions);
}
}
catch (InvalidRequestException e) {
throw new RakamException(e.getMessage(),
HttpResponseStatus.valueOf(e.getStatusCode()));
}
catch (StripeException e) {
throw new RakamException(e.getMessage(), BAD_REQUEST);
}
return customer.getSubscriptions().getData().stream().map(subs ->
new UserSubscription(
subs.getPlan().getId(),
subs.getPlan().getAmount(),
Instant.ofEpochMilli(subs.getCurrentPeriodStart()),
Instant.ofEpochMilli(subs.getCurrentPeriodEnd()),
Optional.ofNullable(subs.getDiscount())
.map(e -> e.getCoupon())
.map(e -> new RakamCoupon(e.getPercentOff(), e.getAmountOff())).orElse(null)))
.collect(Collectors.toList());
// TODO: hasmore?
}
@JsonRequest
@ProtectEndpoint(writeOperation = true, requiresProject = false)
@Path("/me")
public List<UserSubscription> me(
@HeaderParam("X-Requested-With") String csrfHeader,
@javax.inject.Named("user_id") UIPermissionParameterProvider.Project project)
{
if (!"XMLHttpRequest".equals(csrfHeader)) {
throw new RakamException(FORBIDDEN);
}
Optional<WebUser> webUser = service.getUser(project.userId);
if (!webUser.isPresent() || webUser.get().readOnly) {
throw new RakamException("User is not allowed to perform this operation", UNAUTHORIZED);
}
String userStripeId = service.getUserStripeId(project.userId);
if (userStripeId == null) {
return ImmutableList.of();
}
try {
Customer customer = Customer.retrieve(userStripeId, requestOptions);
if (customer.getSubscriptions() == null) {
return ImmutableList.of();
}
return customer.getSubscriptions().getData().stream().map(subs ->
new UserSubscription(
subs.getPlan().getId(),
subs.getPlan().getAmount(),
Instant.ofEpochSecond(subs.getCurrentPeriodStart()),
Instant.ofEpochSecond(subs.getCurrentPeriodEnd()),
Optional.ofNullable(subs.getDiscount())
.map(e -> e.getCoupon())
.map(e -> new RakamCoupon(e.getPercentOff(), e.getAmountOff())).orElse(null)))
.collect(Collectors.toList());
// TODO: hasmore
}
catch (StripeException e) {
throw Throwables.propagate(e);
}
}
@JsonRequest
@ProtectEndpoint(writeOperation = true, requiresProject = false)
@Path("/coupon")
public RakamCoupon checkCoupon(
@ApiParam("coupon") String coupon,
@HeaderParam("X-Requested-With") String csrfHeader,
@javax.inject.Named("user_id") UIPermissionParameterProvider.Project project)
{
if (!"XMLHttpRequest".equals(csrfHeader)) {
throw new RakamException(FORBIDDEN);
}
Optional<WebUser> webUser = service.getUser(project.userId);
if (!webUser.isPresent() || webUser.get().readOnly) {
throw new RakamException("User is not allowed to perform this operation", UNAUTHORIZED);
}
try {
Coupon retrieve = Coupon.retrieve(coupon, requestOptions);
return new RakamCoupon(retrieve.getPercentOff(), retrieve.getAmountOff());
}
catch (InvalidRequestException e) {
if (e.getStatusCode() == 404) {
throw new RakamException(NOT_FOUND);
}
throw Throwables.propagate(e);
}
catch (StripeException e) {
throw Throwables.propagate(e);
}
}
public static class RakamCoupon
{
public final Integer percentOff;
public final Integer amountOff;
@JsonCreator
public RakamCoupon(@ApiParam("percentOff") Integer percentOff,
@ApiParam("amountOff") Integer amountOff)
{
this.percentOff = percentOff;
this.amountOff = amountOff;
}
}
public static class RakamPlan
{
public final String id;
public final String name;
public final Integer amount;
public final String description;
@JsonCreator
public RakamPlan(@ApiParam("id") String id,
@ApiParam("name") String name,
@ApiParam("amount") Integer amount,
@ApiParam("description") String description)
{
this.id = id;
this.name = name;
this.amount = amount / 100;
this.description = description;
}
}
public static class UserSubscription
{
public final double amount;
public final Instant currentPeriodStart;
public final Instant currentPeriodEnd;
public final RakamCoupon coupon;
public final String plan;
@JsonCreator
public UserSubscription(
@ApiParam("plan") String plan,
@ApiParam("amount") double amount,
@ApiParam("currentPeriodStart") Instant currentPeriodStart,
@ApiParam("currentPeriodEnd") Instant currentPeriodEnd,
@ApiParam("coupon") RakamCoupon coupon)
{
this.plan = plan;
this.amount = amount / 100.0;
this.currentPeriodStart = currentPeriodStart;
this.currentPeriodEnd = currentPeriodEnd;
this.coupon = coupon;
}
}
}