Skip to content

Commit

Permalink
add subscription usecases
Browse files Browse the repository at this point in the history
  • Loading branch information
gabrmsouza committed Aug 24, 2024
1 parent 0284d4d commit ca21067
Show file tree
Hide file tree
Showing 9 changed files with 673 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package io.github.gabrmsouza.subscription.application.subscription;

import io.github.gabrmsouza.subscription.application.UseCase;
import io.github.gabrmsouza.subscription.domain.subscription.SubscriptionId;

public abstract class CancelSubscription extends UseCase<CancelSubscription.Input, CancelSubscription.Output> {
public interface Input {
String accountId();
}

public interface Output {
String subscriptionStatus();
SubscriptionId subscriptionId();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package io.github.gabrmsouza.subscription.application.subscription;

import io.github.gabrmsouza.subscription.application.UseCase;
import io.github.gabrmsouza.subscription.domain.payment.Transaction;
import io.github.gabrmsouza.subscription.domain.subscription.SubscriptionId;

import java.time.LocalDate;

public abstract class ChargeSubscription extends UseCase<ChargeSubscription.Input, ChargeSubscription.Output> {
public interface Input {
String accountId();
String paymentType();
String creditCardToken();
}

public interface Output {
SubscriptionId subscriptionId();
String subscriptionStatus();
LocalDate subscriptionDueDate();
Transaction paymentTransaction();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package io.github.gabrmsouza.subscription.application.subscription;

import io.github.gabrmsouza.subscription.application.UseCase;
import io.github.gabrmsouza.subscription.domain.subscription.SubscriptionId;

public abstract class CreateSubscription extends UseCase<CreateSubscription.Input, CreateSubscription.Output> {
public interface Input {
String accountId();
Long planId();
}

public interface Output {
SubscriptionId subscriptionId();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package io.github.gabrmsouza.subscription.application.subscription.impl;

import io.github.gabrmsouza.subscription.application.subscription.CancelSubscription;
import io.github.gabrmsouza.subscription.domain.account.AccountId;
import io.github.gabrmsouza.subscription.domain.exceptions.DomainException;
import io.github.gabrmsouza.subscription.domain.subscription.Subscription;
import io.github.gabrmsouza.subscription.domain.subscription.SubscriptionCommand;
import io.github.gabrmsouza.subscription.domain.subscription.SubscriptionGateway;
import io.github.gabrmsouza.subscription.domain.subscription.SubscriptionId;

import java.util.Objects;

public class DefaultCancelSubscription extends CancelSubscription {
private final SubscriptionGateway subscriptionGateway;

public DefaultCancelSubscription(final SubscriptionGateway subscriptionGateway) {
this.subscriptionGateway = Objects.requireNonNull(subscriptionGateway);
}

@Override
public CancelSubscription.Output execute(final CancelSubscription.Input in) {
if (in == null) {
throw new IllegalArgumentException("Input of DefaultCancelSubscription cannot be null");
}

final var aSubscription = this.subscriptionGateway.latestSubscriptionOfAccount(new AccountId(in.accountId()))
.filter(it -> it.accountId().equals(new AccountId(in.accountId())))
.orElseThrow(() -> DomainException.with("Subscription for account %s was not found".formatted(in.accountId())));

aSubscription.execute(new SubscriptionCommand.CancelSubscription());
this.subscriptionGateway.save(aSubscription);
return new StdOutput(aSubscription);
}

record StdOutput(
SubscriptionId subscriptionId,
String subscriptionStatus
) implements CancelSubscription.Output {
public StdOutput(Subscription aSubscription) {
this(aSubscription.id(), aSubscription.status().value());
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
package io.github.gabrmsouza.subscription.application.subscription.impl;

import io.github.gabrmsouza.subscription.application.subscription.ChargeSubscription;
import io.github.gabrmsouza.subscription.domain.account.Account;
import io.github.gabrmsouza.subscription.domain.account.AccountGateway;
import io.github.gabrmsouza.subscription.domain.account.AccountId;
import io.github.gabrmsouza.subscription.domain.exceptions.DomainException;
import io.github.gabrmsouza.subscription.domain.payment.BillingAddress;
import io.github.gabrmsouza.subscription.domain.payment.Payment;
import io.github.gabrmsouza.subscription.domain.payment.PaymentGateway;
import io.github.gabrmsouza.subscription.domain.payment.Transaction;
import io.github.gabrmsouza.subscription.domain.plan.Plan;
import io.github.gabrmsouza.subscription.domain.plan.PlanGateway;
import io.github.gabrmsouza.subscription.domain.subscription.SubscriptionCommand;
import io.github.gabrmsouza.subscription.domain.subscription.SubscriptionGateway;
import io.github.gabrmsouza.subscription.domain.subscription.SubscriptionId;
import io.github.gabrmsouza.subscription.domain.utils.IDUtils;

import java.time.*;
import java.time.temporal.ChronoUnit;
import java.util.Objects;

public class DefaultChargeSubscription extends ChargeSubscription {
private static final int MAX_INCOMPLETE_DAYS = 2;

private final AccountGateway accountGateway;
private final Clock clock;
private final PaymentGateway paymentGateway;
private final PlanGateway planGateway;
private final SubscriptionGateway subscriptionGateway;

public DefaultChargeSubscription(
final AccountGateway accountGateway,
final Clock clock,
final PaymentGateway paymentGateway,
final PlanGateway planGateway,
final SubscriptionGateway subscriptionGateway
) {
this.accountGateway = Objects.requireNonNull(accountGateway);
this.clock = Objects.requireNonNull(clock);
this.paymentGateway = Objects.requireNonNull(paymentGateway);
this.planGateway = Objects.requireNonNull(planGateway);
this.subscriptionGateway = Objects.requireNonNull(subscriptionGateway);
}

@Override
public ChargeSubscription.Output execute(final ChargeSubscription.Input in) {
if (in == null) {
throw new IllegalArgumentException("Input of DefaultChargeSubscription cannot be null");
}

final var accountId = new AccountId(in.accountId());
final var now = clock.instant();

final var aSubscription = subscriptionGateway.latestSubscriptionOfAccount(accountId)
.filter(it -> it.accountId().equals(accountId))
.orElseThrow(() -> DomainException.with("Subscription for account %s was not found".formatted(in.accountId())));

if (aSubscription.dueDate().isAfter(LocalDate.ofInstant(now, ZoneId.systemDefault()))) {
return new StdOutput(aSubscription.id(), aSubscription.status().value(), aSubscription.dueDate(), null);
}

final var aPlan = this.planGateway.planOfId(aSubscription.planId())
.orElseThrow(() -> DomainException.notFound(Plan.class, aSubscription.planId()));

final var anUserAccount = this.accountGateway.accountOfId(accountId)
.orElseThrow(() -> DomainException.notFound(Account.class, accountId));

final var aPayment = this.newPaymentWith(in, aPlan, anUserAccount);
final var actualTransaction = this.paymentGateway.processPayment(aPayment);

if (actualTransaction.isSuccess()) {
aSubscription.execute(new SubscriptionCommand.RenewSubscription(aPlan, actualTransaction.transactionId()));
} else if (hasTolerableDays(aSubscription.dueDate(), now)) {
aSubscription.execute(new SubscriptionCommand.IncompleteSubscription(actualTransaction.errorMessage(), actualTransaction.transactionId()));
} else {
aSubscription.execute(new SubscriptionCommand.CancelSubscription());
}

this.subscriptionGateway.save(aSubscription);
return new StdOutput(aSubscription.id(), aSubscription.status().value(), aSubscription.dueDate(), actualTransaction);
}

private boolean hasTolerableDays(final LocalDate dueDate, final Instant now) {
return ChronoUnit.DAYS.between(dueDate, LocalDate.ofInstant(now, ZoneOffset.UTC)) <= MAX_INCOMPLETE_DAYS;
}

private Payment newPaymentWith(final Input in, final Plan aPlan, final Account anUserAccount) {
return Payment.create(
in.paymentType(),
IDUtils.uuid(),
aPlan.price().amount(),
new BillingAddress(
anUserAccount.billingAddress().zipcode(),
anUserAccount.billingAddress().number(),
anUserAccount.billingAddress().complement(),
anUserAccount.billingAddress().country()
),
in.creditCardToken()
);
}

record StdOutput(
SubscriptionId subscriptionId,
String subscriptionStatus,
LocalDate subscriptionDueDate,
Transaction paymentTransaction
) implements ChargeSubscription.Output {}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
package io.github.gabrmsouza.subscription.application.subscription.impl;

import io.github.gabrmsouza.subscription.application.subscription.CreateSubscription;
import io.github.gabrmsouza.subscription.domain.account.Account;
import io.github.gabrmsouza.subscription.domain.account.AccountGateway;
import io.github.gabrmsouza.subscription.domain.account.AccountId;
import io.github.gabrmsouza.subscription.domain.exceptions.DomainException;
import io.github.gabrmsouza.subscription.domain.plan.Plan;
import io.github.gabrmsouza.subscription.domain.plan.PlanGateway;
import io.github.gabrmsouza.subscription.domain.plan.PlanId;
import io.github.gabrmsouza.subscription.domain.subscription.Subscription;
import io.github.gabrmsouza.subscription.domain.subscription.SubscriptionGateway;
import io.github.gabrmsouza.subscription.domain.subscription.SubscriptionId;

public class DefaultCreateSubscription extends CreateSubscription {
private final AccountGateway accountGateway;
private final PlanGateway planGateway;
private final SubscriptionGateway subscriptionGateway;

public DefaultCreateSubscription(
final AccountGateway accountGateway,
final PlanGateway planGateway,
final SubscriptionGateway subscriptionGateway
) {
this.accountGateway = accountGateway;
this.planGateway = planGateway;
this.subscriptionGateway = subscriptionGateway;
}

@Override
public CreateSubscription.Output execute(final CreateSubscription.Input in) {
if (in == null) {
throw new IllegalArgumentException("Input of DefaultCreateSubscription should not be null");
}

final var accountId = new AccountId(in.accountId());
final var planId = new PlanId(in.planId());

validateActiveSubscription(in, accountId);

final var aPlan = this.planGateway.planOfId(planId)
.filter(Plan::active)
.orElseThrow(() -> DomainException.notFound(Plan.class, planId));

final var anUserAccount = this.accountGateway.accountOfId(accountId)
.orElseThrow(() -> DomainException.notFound(Account.class, accountId));

final var aNewSubscription = this.newSubscriptionWith(anUserAccount, aPlan);
this.subscriptionGateway.save(aNewSubscription);
return new StdOutput(aNewSubscription.id());
}

private Subscription newSubscriptionWith(final Account anUserAccount, final Plan aPlan) {
return Subscription.newSubscription(
this.subscriptionGateway.nextId(),
anUserAccount.id(),
aPlan
);
}

private void validateActiveSubscription(Input in, AccountId accountId) {
this.subscriptionGateway
.latestSubscriptionOfAccount(accountId)
.ifPresent(sub -> {
if (!sub.isCanceled()) {
throw DomainException.with("Account %s already has a active subscription".formatted(in.accountId()));
}
});
}

public record StdOutput(SubscriptionId subscriptionId) implements CreateSubscription.Output {
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package io.github.gabrmsouza.subscription.application.subscription.impl;

import io.github.gabrmsouza.subscription.application.UseCaseTest;
import io.github.gabrmsouza.subscription.application.subscription.CancelSubscription;
import io.github.gabrmsouza.subscription.domain.Fixture;
import io.github.gabrmsouza.subscription.domain.account.AccountId;
import io.github.gabrmsouza.subscription.domain.plan.Plan;
import io.github.gabrmsouza.subscription.domain.subscription.Subscription;
import io.github.gabrmsouza.subscription.domain.subscription.SubscriptionGateway;
import io.github.gabrmsouza.subscription.domain.subscription.SubscriptionId;
import io.github.gabrmsouza.subscription.domain.subscription.status.ActiveSubscriptionStatus;
import io.github.gabrmsouza.subscription.domain.subscription.status.CanceledSubscriptionStatus;
import org.junit.jupiter.api.Test;
import org.mockito.InjectMocks;
import org.mockito.Mock;

import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.util.Optional;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.AdditionalAnswers.returnsFirstArg;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.when;

class DefaultCancelSubscriptionTest extends UseCaseTest {
@Mock
private SubscriptionGateway subscriptionGateway;

@InjectMocks
private DefaultCancelSubscription target;

@Test
public void givenActiveSubscription_whenCallsCancelSubscription_shouldCancelIt() {
// given
var expectedPlan = Fixture.Plans.plus();
var expectedAccount = Fixture.Accounts.john();
var expectedSubscription = newSubscriptionWith(expectedAccount.id(), expectedPlan, ActiveSubscriptionStatus.ACTIVE, LocalDateTime.now().minusDays(15));
var expectedSubscriptionId = expectedSubscription.id();
var expectedSubscriptionStatus = CanceledSubscriptionStatus.CANCELED;

when(subscriptionGateway.latestSubscriptionOfAccount(eq(expectedAccount.id()))).thenReturn(Optional.of(expectedSubscription));
when(subscriptionGateway.save(any())).thenAnswer(returnsFirstArg());

// when
var actualOutput =
this.target.execute(new CancelSubscriptionTestInput(expectedSubscriptionId.value(), expectedAccount.id().value()));

// then
assertEquals(expectedSubscriptionId, actualOutput.subscriptionId());
assertEquals(expectedSubscriptionStatus, actualOutput.subscriptionStatus());
}

private static Subscription newSubscriptionWith(AccountId expectedAccountId, Plan plus, String status, LocalDateTime date) {
final var instant = date.toInstant(ZoneOffset.UTC);
return Subscription.with(
new SubscriptionId("SUB123"), 1, expectedAccountId, plus.id(),
date.toLocalDate(), status,
instant, "a123",
instant, instant
);
}

record CancelSubscriptionTestInput(String subscriptionId, String accountId) implements CancelSubscription.Input {
}
}
Loading

0 comments on commit ca21067

Please sign in to comment.