From a1a3ea01002edb8cb6f099c18c6ad115203804da Mon Sep 17 00:00:00 2001 From: cristianpela Date: Tue, 13 Oct 2020 15:44:46 +0300 Subject: [PATCH 1/2] #606 Implemented and tested Wallet#pay(). --- .../main/java/com/selfxdsd/api/Wallet.java | 78 ++------- .../exceptions/WalletPaymentException.java | 50 ++++++ .../selfxdsd/core/projects/StripeWallet.java | 149 ++++++++++++++++-- .../selfxdsd/core/MissingWalletTestCase.java | 33 ++-- .../projects/StoredPaymentMethodTestCase.java | 2 +- .../core/projects/StripeWalletITCase.java | 146 +++++++++++++++++ .../core/projects/StripeWalletTestCase.java | 141 ++++++++++++++--- 7 files changed, 491 insertions(+), 108 deletions(-) create mode 100644 self-api/src/main/java/com/selfxdsd/api/exceptions/WalletPaymentException.java create mode 100644 self-core-impl/src/test/java/com/selfxdsd/core/projects/StripeWalletITCase.java diff --git a/self-api/src/main/java/com/selfxdsd/api/Wallet.java b/self-api/src/main/java/com/selfxdsd/api/Wallet.java index 43ac6db0..1c74217d 100644 --- a/self-api/src/main/java/com/selfxdsd/api/Wallet.java +++ b/self-api/src/main/java/com/selfxdsd/api/Wallet.java @@ -24,11 +24,11 @@ import com.selfxdsd.api.exceptions.InvoiceException; import com.selfxdsd.api.exceptions.PaymentMethodsException; +import com.selfxdsd.api.exceptions.WalletPaymentException; import java.math.BigDecimal; -import java.time.LocalDateTime; +import java.math.RoundingMode; import java.util.Iterator; -import java.util.UUID; /** * A project's wallet. @@ -66,10 +66,10 @@ default BigDecimal debt() { /** * Pay an invoice. * @param invoice The Invoice to be paid. - * @return The paid Invoice containing the payment time and transaction ID. + * @return Wallet having cash deducted with Invoice amount. * @throws InvoiceException.AlreadyPaid If the Invoice is already paid. */ - Invoice pay(final Invoice invoice); + Wallet pay(final Invoice invoice); /** * Type of this wallet. @@ -158,66 +158,22 @@ public BigDecimal cash() { } @Override - public Invoice pay(final Invoice invoice) { + public Wallet pay(final Invoice invoice) { if(invoice.isPaid()) { throw new InvoiceException.AlreadyPaid(invoice); } - final LocalDateTime paymentTime = LocalDateTime.now(); - final String transactionId = "fk-" + UUID - .randomUUID() - .toString() - .replace("-", ""); - return new Invoice() { - @Override - public int invoiceId() { - return invoice.invoiceId(); - } - - @Override - public InvoicedTask register( - final Task task, - final BigDecimal commission - ) { - throw new IllegalStateException( - "Invoice is already paid, can't add a new Task to it!" - ); - } - - @Override - public Contract contract() { - return invoice.contract(); - } - - @Override - public LocalDateTime createdAt() { - return invoice.createdAt(); - } - - @Override - public LocalDateTime paymentTime() { - return paymentTime; - } - - @Override - public String transactionId() { - return transactionId; - } - - @Override - public InvoicedTasks tasks() { - return invoice.tasks(); - } - - @Override - public BigDecimal totalAmount() { - return invoice.totalAmount(); - } - - @Override - public boolean isPaid() { - return true; - } - }; + final BigDecimal newCash = this.cash.subtract(invoice + .totalAmount()); + if (newCash.longValueExact() < 0L) { + throw new WalletPaymentException("No cash available in wallet " + + "for paying invoice #" + invoice.invoiceId() + + ". Please increase the limit from your dashboard with" + + " at least " + newCash.abs().divide(BigDecimal + .valueOf(1000), RoundingMode.HALF_UP) + "$." + ); + } + return new Missing(this.project, newCash, this.active, + this.identifier); } @Override diff --git a/self-api/src/main/java/com/selfxdsd/api/exceptions/WalletPaymentException.java b/self-api/src/main/java/com/selfxdsd/api/exceptions/WalletPaymentException.java new file mode 100644 index 00000000..91cf71b8 --- /dev/null +++ b/self-api/src/main/java/com/selfxdsd/api/exceptions/WalletPaymentException.java @@ -0,0 +1,50 @@ +/** + * Copyright (c) 2020, Self XDSD Contributors + * All rights reserved. + *

+ * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), + * to read the Software only. Permission is hereby NOT GRANTED to use, copy, + * modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software. + *

+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, + * OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT + * OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ +package com.selfxdsd.api.exceptions; + +/** + * Exception thrown during payment of an Invoice. + * @author criske + * @version $Id$ + * @since 0.0.28 + */ +public final class WalletPaymentException extends SelfException { + + /** + * Message. + */ + private final String message; + + /** + * Ctor. + * @param message Message. + */ + public WalletPaymentException(final String message) { + this.message = message; + } + + @Override + String getSelfMessage() { + return this.message; + } +} diff --git a/self-core-impl/src/main/java/com/selfxdsd/core/projects/StripeWallet.java b/self-core-impl/src/main/java/com/selfxdsd/core/projects/StripeWallet.java index 53d797eb..c9144227 100644 --- a/self-core-impl/src/main/java/com/selfxdsd/core/projects/StripeWallet.java +++ b/self-core-impl/src/main/java/com/selfxdsd/core/projects/StripeWallet.java @@ -22,22 +22,27 @@ */ package com.selfxdsd.core.projects; -import com.selfxdsd.api.Invoice; -import com.selfxdsd.api.PaymentMethods; -import com.selfxdsd.api.Project; -import com.selfxdsd.api.Wallet; +import com.selfxdsd.api.*; +import com.selfxdsd.api.exceptions.InvoiceException; +import com.selfxdsd.api.exceptions.WalletPaymentException; import com.selfxdsd.api.storage.Storage; +import com.selfxdsd.core.Env; +import com.selfxdsd.core.contracts.invoices.StoredInvoice; +import com.stripe.Stripe; +import com.stripe.exception.StripeException; +import com.stripe.model.PaymentIntent; +import com.stripe.param.PaymentIntentCreateParams; import java.math.BigDecimal; +import java.math.RoundingMode; +import java.time.LocalDateTime; +import java.time.OffsetDateTime; /** * A Project's Stripe wallet. * @author Mihai Andronache (amihaiemil@gmail.com) * @version $Id$ * @since 0.0.27 - * @todo #604:60min Implement method pay(...) here as soon as - * we have a Wallets PaymentMethods available. We should always - * try to use the active PaymentMethod first. * @todo #609:15min Implement equals() and hashCode() for StripeWallet since * StoredPaymentMethod is using Wallet for its equals and hashCode methods. */ @@ -68,6 +73,11 @@ public final class StripeWallet implements Wallet { */ private final String identifier; + /** + * Stripe API token. + */ + private final String stripeApiToken; + /** * Ctor. * @param storage Self storage. @@ -75,19 +85,45 @@ public final class StripeWallet implements Wallet { * @param limit Cash limit we're allowed to use. * @param identifier Wallet identifier from Stripe's side. * @param active Is this wallet active or not? + * @param stripeApiToken Stripe API token. */ - public StripeWallet( + StripeWallet( final Storage storage, final Project project, final BigDecimal limit, final String identifier, - final boolean active + final boolean active, + final String stripeApiToken ) { this.storage = storage; this.project = project; this.identifier = identifier; this.limit = limit; this.active = active; + this.stripeApiToken = stripeApiToken; + } + + /** + * Ctor. + * @param storage Self storage. + * @param project Project to which this wallet belongs/ + * @param limit Cash limit we're allowed to use. + * @param identifier Wallet identifier from Stripe's side. + * @param active Is this wallet active or not? + */ + public StripeWallet( + final Storage storage, + final Project project, + final BigDecimal limit, + final String identifier, + final boolean active + ) { + this(storage, + project, + limit, + identifier, + active, + System.getenv(Env.STRIPE_API_TOKEN)); } @Override @@ -96,8 +132,97 @@ public BigDecimal cash() { } @Override - public Invoice pay(final Invoice invoice) { - throw new UnsupportedOperationException("Not yet implemented."); + public Wallet pay(final Invoice invoice) { + if (invoice.isPaid()) { + throw new InvoiceException.AlreadyPaid(invoice); + } + + final BigDecimal newLimit = this.limit.subtract(invoice.totalAmount()); + if (newLimit.longValueExact() < 0L) { + throw new WalletPaymentException("No cash available in wallet " + + "for paying invoice #" + invoice.invoiceId() + + ". Please increase the limit from your dashboard with" + + " at least " + newLimit.abs() + .divide(BigDecimal.valueOf(1000), RoundingMode.HALF_UP) + "$." + ); + } + + ensureApiToken(); + + try { + final Contributor contributor = invoice.contract().contributor(); + final PayoutMethod payoutMethod = this.storage + .payoutMethods() + .ofContributor(contributor) + .active(); + if (payoutMethod == null) { + throw new WalletPaymentException( + "No active payout method for contributor " + + contributor.username() + ); + } + final PaymentMethod paymentMethod = this.storage + .paymentMethods() + .ofWallet(this) + .active(); + if (paymentMethod == null) { + throw new WalletPaymentException( + "No active payment method for wallet #" + + this.identifier + " of project " + + this.project.repoFullName() + "/" + + this.project.provider() + ); + } + final PaymentIntent paymentIntent = PaymentIntent + .create(PaymentIntentCreateParams.builder() + .setCurrency("usd") + .setAmount(invoice.totalAmount().longValueExact()) + .setCustomer(payoutMethod.identifier()) + .setPaymentMethod(paymentMethod.identifier()) + .setConfirm(true) + .build()); + + final String status = paymentIntent.getStatus(); + if ("succeeded".equals(status) || "processing".equals(status)) { + final LocalDateTime paymentDate = LocalDateTime + .ofEpochSecond(paymentIntent.getCreated(), + 0, OffsetDateTime.now().getOffset()); + this.storage.invoices() + .registerAsPaid(new StoredInvoice( + invoice.invoiceId(), + invoice.contract(), + invoice.createdAt(), + paymentDate, + paymentIntent.getId(), + this.storage) + ); + } else { + throw new WalletPaymentException( + "Could not pay invoice #" + invoice.invoiceId() + " due to" + + " Stripe payment intent status \"" + status + "\"" + ); + } + } catch (final StripeException ex) { + throw new IllegalStateException( + "Stripe threw an exception when trying execute PaymentIntent" + + " for invoice #" + invoice.invoiceId(), + ex + ); + } + return this.updateCash(newLimit); + } + + /** + * Ensure that Stripe API token is set. + */ + private void ensureApiToken() { + if (this.stripeApiToken == null + || this.stripeApiToken.trim().isEmpty()) { + throw new WalletPaymentException( + "Please specify the self_stripe_token Environment Variable!" + ); + } + Stripe.apiKey = this.stripeApiToken; } @Override @@ -125,6 +250,6 @@ public Wallet updateCash(final BigDecimal cash) { @Override public PaymentMethods paymentMethods() { - throw new UnsupportedOperationException("Not implemented yet"); + return this.storage.paymentMethods().ofWallet(this); } } diff --git a/self-core-impl/src/test/java/com/selfxdsd/core/MissingWalletTestCase.java b/self-core-impl/src/test/java/com/selfxdsd/core/MissingWalletTestCase.java index bb30ba37..f7a26cda 100644 --- a/self-core-impl/src/test/java/com/selfxdsd/core/MissingWalletTestCase.java +++ b/self-core-impl/src/test/java/com/selfxdsd/core/MissingWalletTestCase.java @@ -24,6 +24,7 @@ import com.selfxdsd.api.*; import com.selfxdsd.api.exceptions.InvoiceException; +import com.selfxdsd.api.exceptions.WalletPaymentException; import org.hamcrest.MatcherAssert; import org.hamcrest.Matchers; import org.junit.Test; @@ -128,18 +129,12 @@ public void paysInvoice() { ); final Invoice invoice = Mockito.mock(Invoice.class); Mockito.when(invoice.isPaid()).thenReturn(Boolean.FALSE); + Mockito.when(invoice.totalAmount()).thenReturn(BigDecimal.valueOf(1)); - final Invoice paid = wallet.pay(invoice); + final Wallet paid = wallet.pay(invoice); - MatcherAssert.assertThat(paid.isPaid(), Matchers.is(Boolean.TRUE)); - MatcherAssert.assertThat(paid.paymentTime(), Matchers.notNullValue()); - MatcherAssert.assertThat( - paid.transactionId(), - Matchers.allOf( - Matchers.notNullValue(), - Matchers.startsWith("fk-") - ) - ); + MatcherAssert.assertThat(paid.cash(), Matchers + .equalTo(BigDecimal.valueOf(99_999_999))); } /** @@ -159,6 +154,24 @@ public void complainsDoublePayment() { wallet.pay(paid); } + /** + * The Missing wallet should throw an exception if we try to pay + * an Invoice and there are not enough cash. + */ + @Test(expected = WalletPaymentException.class) + public void complainsWhenNoCashAvailable() { + final Wallet wallet = new Wallet.Missing( + Mockito.mock(Project.class), + BigDecimal.valueOf(100_000_000), + Boolean.TRUE, + "fake-123w" + ); + final Invoice invoice = Mockito.mock(Invoice.class); + Mockito.when(invoice.totalAmount()) + .thenReturn(BigDecimal.valueOf(100_000_001)); + wallet.pay(invoice); + } + /** * Mock a Contract. * @param value Value of the contract. diff --git a/self-core-impl/src/test/java/com/selfxdsd/core/projects/StoredPaymentMethodTestCase.java b/self-core-impl/src/test/java/com/selfxdsd/core/projects/StoredPaymentMethodTestCase.java index cba312a8..7a015222 100644 --- a/self-core-impl/src/test/java/com/selfxdsd/core/projects/StoredPaymentMethodTestCase.java +++ b/self-core-impl/src/test/java/com/selfxdsd/core/projects/StoredPaymentMethodTestCase.java @@ -155,7 +155,7 @@ public BigDecimal cash() { } @Override - public Invoice pay(final Invoice invoice) { + public Wallet pay(final Invoice invoice) { throw new UnsupportedOperationException(); } diff --git a/self-core-impl/src/test/java/com/selfxdsd/core/projects/StripeWalletITCase.java b/self-core-impl/src/test/java/com/selfxdsd/core/projects/StripeWalletITCase.java new file mode 100644 index 00000000..be8e4e04 --- /dev/null +++ b/self-core-impl/src/test/java/com/selfxdsd/core/projects/StripeWalletITCase.java @@ -0,0 +1,146 @@ +/** + * Copyright (c) 2020, Self XDSD Contributors + * All rights reserved. + *

+ * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), + * to read the Software only. Permission is hereby NOT GRANTED to use, copy, + * modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software. + *

+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, + * OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT + * OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ +package com.selfxdsd.core.projects; + +import com.jcabi.http.mock.MkAnswer; +import com.jcabi.http.mock.MkContainer; +import com.jcabi.http.mock.MkGrizzlyContainer; +import com.selfxdsd.api.*; +import com.selfxdsd.api.storage.Storage; +import com.selfxdsd.core.RandomPort; +import com.stripe.Stripe; +import org.junit.Rule; +import org.junit.Test; +import org.mockito.Mockito; + +import javax.json.Json; +import java.io.IOException; +import java.math.BigDecimal; +import java.net.HttpURLConnection; +import java.time.LocalDateTime; + +/** + * Integration tests for {@link StripeWallet} payment. + * @author criske + * @version $Id$ + * @since 0.0.28 + * @checkstyle ExecutableStatementCount (1000 lines). + */ +public final class StripeWalletITCase { + + /** + * The rule for skipping test if there's BindException. + * @checkstyle VisibilityModifierCheck (3 lines) + */ + @Rule + public final RandomPort resource = new RandomPort(); + + /** + * The StripeWallet successfully paying the Invoice. + * @throws IOException if something went wrong. + */ + @Test + public void payingInvoiceOk() throws IOException { + try( + final MkContainer container = new MkGrizzlyContainer() + .next( + new MkAnswer.Simple( + HttpURLConnection.HTTP_OK, + Json.createObjectBuilder() + .add("id", "pi_1DiKBG2eZvKYlo2CRcpuIHvu") + .add("created", 1545045758) + .add("status", "succeeded") + .build() + .toString() + ) + ).start(this.resource.port()) + ) { + Stripe.overrideUploadBase(container.home().toString()); + Stripe.overrideApiBase(container.home().toString()); + final LocalDateTime now = LocalDateTime.now(); + final Invoice invoice = Mockito.mock(Invoice.class); + Mockito.when(invoice.invoiceId()).thenReturn(1); + Mockito.when(invoice.createdAt()).thenReturn(now); + Mockito.when(invoice.isPaid()).thenReturn(false); + Mockito.when(invoice.totalAmount()).thenReturn(BigDecimal.TEN); + + final Contract contract = Mockito.mock(Contract.class); + final Contributor contributor = Mockito.mock(Contributor.class); + Mockito.when(contract.contributor()).thenReturn(contributor); + Mockito.when(invoice.contract()).thenReturn(contract); + + final Storage storage = Mockito.mock(Storage.class); + final PayoutMethods allPayoutsMethods = Mockito + .mock(PayoutMethods.class); + final PayoutMethods payoutsOfContrib = Mockito + .mock(PayoutMethods.class); + final PayoutMethod payoutMethod = Mockito.mock(PayoutMethod.class); + final PaymentMethods allPaymentMethods = Mockito + .mock(PaymentMethods.class); + final PaymentMethods paymentsOfWallet = Mockito + .mock(PaymentMethods.class); + final PaymentMethod paymentMethod = Mockito + .mock(PaymentMethod.class); + + final Invoices invoices = Mockito.mock(Invoices.class); + Mockito.when(storage.invoices()).thenReturn(invoices); + + final Project project = Mockito.mock(Project.class); + final Wallets allWallets = Mockito.mock(Wallets.class); + final Wallets ofProject = Mockito.mock(Wallets.class); + + final Wallet stripe = new StripeWallet( + storage, + project, + BigDecimal.valueOf(1000), + "123StripeID", + Boolean.TRUE, + "stripe_24343" + ); + + Mockito.when(storage.payoutMethods()).thenReturn(allPayoutsMethods); + Mockito.when(allPayoutsMethods.ofContributor(contributor)) + .thenReturn(payoutsOfContrib); + Mockito.when(payoutsOfContrib.active()).thenReturn(payoutMethod); + Mockito.when(payoutMethod.identifier()).thenReturn("ac_123"); + + Mockito.when(storage.paymentMethods()) + .thenReturn(allPaymentMethods); + Mockito.when(allPaymentMethods.ofWallet(stripe)) + .thenReturn(paymentsOfWallet); + Mockito.when(paymentsOfWallet.active()).thenReturn(paymentMethod); + Mockito.when(paymentMethod.identifier()).thenReturn("pm_123"); + + Mockito.when(storage.wallets()).thenReturn(allWallets); + Mockito.when(allWallets.ofProject(project)).thenReturn(ofProject); + + stripe.pay(invoice); + + Mockito.verify(invoices, Mockito.times(1)) + .registerAsPaid(Mockito.any(Invoice.class)); + Mockito.verify(ofProject, Mockito.times(1)) + .updateCash(stripe, BigDecimal.valueOf(990)); + } + } + +} diff --git a/self-core-impl/src/test/java/com/selfxdsd/core/projects/StripeWalletTestCase.java b/self-core-impl/src/test/java/com/selfxdsd/core/projects/StripeWalletTestCase.java index 3201aa59..d61f322e 100644 --- a/self-core-impl/src/test/java/com/selfxdsd/core/projects/StripeWalletTestCase.java +++ b/self-core-impl/src/test/java/com/selfxdsd/core/projects/StripeWalletTestCase.java @@ -22,10 +22,9 @@ */ package com.selfxdsd.core.projects; -import com.selfxdsd.api.Invoice; -import com.selfxdsd.api.Project; -import com.selfxdsd.api.Wallet; -import com.selfxdsd.api.Wallets; +import com.selfxdsd.api.*; +import com.selfxdsd.api.exceptions.InvoiceException; +import com.selfxdsd.api.exceptions.WalletPaymentException; import com.selfxdsd.api.storage.Storage; import org.hamcrest.MatcherAssert; import org.hamcrest.Matchers; @@ -39,6 +38,7 @@ * @author Mihai Andronache (amihaiemil@gmail.com) * @version $Id$ * @since 0.0.27 + * @checkstyle ExecutableStatementCount (1000 lines). */ public final class StripeWalletTestCase { @@ -97,21 +97,6 @@ public void returnsActiveFlag() { ); } - /** - * Pay method is not yet supported. - */ - @Test (expected = UnsupportedOperationException.class) - public void payIsNotYetSupported() { - final Wallet stripe = new StripeWallet( - Mockito.mock(Storage.class), - Mockito.mock(Project.class), - BigDecimal.valueOf(1000), - "123StripeID", - Boolean.TRUE - ); - stripe.pay(Mockito.mock(Invoice.class)); - } - /** * Wallet cash limit can be updated. */ @@ -146,17 +131,125 @@ public void updatesCash() { } /** - * Payment methods is not yet supported. + * Wallet has payment methods. */ - @Test (expected = UnsupportedOperationException.class) - public void paymentMethodsAreNotSupported(){ + @Test + public void hasPaymentMethods(){ + final PaymentMethods all = Mockito.mock(PaymentMethods.class); + final PaymentMethods ofWallet = Mockito.mock(PaymentMethods.class); + final Storage storage = Mockito.mock(Storage.class); final Wallet stripe = new StripeWallet( - Mockito.mock(Storage.class), + storage, Mockito.mock(Project.class), BigDecimal.valueOf(1000), "123StripeID", Boolean.TRUE ); - stripe.paymentMethods(); + + Mockito.when(storage.paymentMethods()).thenReturn(all); + Mockito.when(all.ofWallet(stripe)).thenReturn(ofWallet); + + MatcherAssert.assertThat(stripe.paymentMethods(), + Matchers.equalTo(ofWallet)); + } + + /** + * Wallet.pay(...) throws if the Invoice is already paid. + */ + @Test(expected = InvoiceException.AlreadyPaid.class) + public void complainsIfInvoiceIsAlreadyPaid(){ + final Invoice invoice = Mockito.mock(Invoice.class); + Mockito.when(invoice.isPaid()).thenReturn(true); + + new StripeWallet( + Mockito.mock(Storage.class), + Mockito.mock(Project.class), + BigDecimal.TEN, + "id", + true + ).pay(invoice); + } + + /** + * Wallet.pay(...) throws if the is no active payout method. + */ + @Test(expected = WalletPaymentException.class) + public void complainsIfThereIsNoActivePayout(){ + final Invoice invoice = Mockito.mock(Invoice.class); + Mockito.when(invoice.isPaid()).thenReturn(false); + Mockito.when(invoice.totalAmount()).thenReturn(BigDecimal.TEN); + + final Storage storage = Mockito.mock(Storage.class); + + final Contract contract = Mockito.mock(Contract.class); + final Contributor contributor = Mockito.mock(Contributor.class); + Mockito.when(contract.contributor()).thenReturn(contributor); + Mockito.when(invoice.contract()).thenReturn(contract); + + final PayoutMethods allPayoutsMethods = Mockito + .mock(PayoutMethods.class); + final PayoutMethods payoutsOfContrib = Mockito + .mock(PayoutMethods.class); + + Mockito.when(storage.payoutMethods()).thenReturn(allPayoutsMethods); + Mockito.when(allPayoutsMethods.ofContributor(contributor)) + .thenReturn(payoutsOfContrib); + + new StripeWallet( + storage, + Mockito.mock(Project.class), + BigDecimal.TEN, + "id", + true, + "stripe_token_123" + ).pay(invoice); + } + + /** + * Wallet.pay(...) throws if the is no active payment method. + */ + @Test(expected = WalletPaymentException.class) + public void complainsIfThereIsNoActivePaymentMethod(){ + final Invoice invoice = Mockito.mock(Invoice.class); + Mockito.when(invoice.isPaid()).thenReturn(false); + Mockito.when(invoice.totalAmount()).thenReturn(BigDecimal.TEN); + + final Storage storage = Mockito.mock(Storage.class); + + final Contract contract = Mockito.mock(Contract.class); + final Contributor contributor = Mockito.mock(Contributor.class); + Mockito.when(contract.contributor()).thenReturn(contributor); + Mockito.when(invoice.contract()).thenReturn(contract); + + final PayoutMethods allPayoutsMethods = Mockito + .mock(PayoutMethods.class); + final PayoutMethods payoutsOfContrib = Mockito + .mock(PayoutMethods.class); + final PayoutMethod payoutMethod = Mockito.mock(PayoutMethod.class); + + Mockito.when(storage.payoutMethods()).thenReturn(allPayoutsMethods); + Mockito.when(allPayoutsMethods.ofContributor(contributor)) + .thenReturn(payoutsOfContrib); + Mockito.when(payoutsOfContrib.active()).thenReturn(payoutMethod); + Mockito.when(payoutMethod.identifier()).thenReturn("ac_123"); + + final Wallet stripe = new StripeWallet( + storage, + Mockito.mock(Project.class), + BigDecimal.TEN, + "id", + true, + "stripe_token_123" + ); + final PaymentMethods allPaymentMethods = Mockito + .mock(PaymentMethods.class); + final PaymentMethods paymentsOfWallet = Mockito + .mock(PaymentMethods.class); + Mockito.when(storage.paymentMethods()) + .thenReturn(allPaymentMethods); + Mockito.when(allPaymentMethods.ofWallet(stripe)) + .thenReturn(paymentsOfWallet); + + stripe.pay(invoice); } } From 16617a887955bbb05f8560bd736924971191fa53 Mon Sep 17 00:00:00 2001 From: cristianpela Date: Tue, 13 Oct 2020 15:56:29 +0300 Subject: [PATCH 2/2] #606 Implemented and tested Wallet#pay(). - small fix --- .../src/main/java/com/selfxdsd/core/projects/StripeWallet.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/self-core-impl/src/main/java/com/selfxdsd/core/projects/StripeWallet.java b/self-core-impl/src/main/java/com/selfxdsd/core/projects/StripeWallet.java index c9144227..93cb0c97 100644 --- a/self-core-impl/src/main/java/com/selfxdsd/core/projects/StripeWallet.java +++ b/self-core-impl/src/main/java/com/selfxdsd/core/projects/StripeWallet.java @@ -142,7 +142,7 @@ public Wallet pay(final Invoice invoice) { throw new WalletPaymentException("No cash available in wallet " + "for paying invoice #" + invoice.invoiceId() + ". Please increase the limit from your dashboard with" - + " at least " + newLimit.abs() + + " at least " + newLimit.abs().add(invoice.totalAmount()) .divide(BigDecimal.valueOf(1000), RoundingMode.HALF_UP) + "$." ); }