-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
0284d4d
commit ca21067
Showing
9 changed files
with
673 additions
and
0 deletions.
There are no files selected for viewing
15 changes: 15 additions & 0 deletions
15
...n/java/io/github/gabrmsouza/subscription/application/subscription/CancelSubscription.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | ||
} | ||
} |
22 changes: 22 additions & 0 deletions
22
...n/java/io/github/gabrmsouza/subscription/application/subscription/ChargeSubscription.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | ||
} | ||
} |
15 changes: 15 additions & 0 deletions
15
...n/java/io/github/gabrmsouza/subscription/application/subscription/CreateSubscription.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | ||
} | ||
} |
43 changes: 43 additions & 0 deletions
43
...thub/gabrmsouza/subscription/application/subscription/impl/DefaultCancelSubscription.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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()); | ||
} | ||
} | ||
} |
109 changes: 109 additions & 0 deletions
109
...thub/gabrmsouza/subscription/application/subscription/impl/DefaultChargeSubscription.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 {} | ||
} |
73 changes: 73 additions & 0 deletions
73
...thub/gabrmsouza/subscription/application/subscription/impl/DefaultCreateSubscription.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 { | ||
} | ||
} |
67 changes: 67 additions & 0 deletions
67
.../gabrmsouza/subscription/application/subscription/impl/DefaultCancelSubscriptionTest.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 { | ||
} | ||
} |
Oops, something went wrong.