Skip to content

Commit

Permalink
Merge branch '__rultor'
Browse files Browse the repository at this point in the history
  • Loading branch information
rultor committed Oct 15, 2020
2 parents a0b168b + 16617a8 commit c24581d
Show file tree
Hide file tree
Showing 7 changed files with 491 additions and 108 deletions.
78 changes: 17 additions & 61 deletions self-api/src/main/java/com/selfxdsd/api/Wallet.java
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
/**
* Copyright (c) 2020, Self XDSD Contributors
* All rights reserved.
* <p>
* 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.
* <p>
* 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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*/
Expand Down Expand Up @@ -68,26 +73,57 @@ public final class StripeWallet implements Wallet {
*/
private final String identifier;

/**
* Stripe API token.
*/
private final String 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?
* @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
Expand All @@ -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().add(invoice.totalAmount())
.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
Expand Down Expand Up @@ -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);
}
}
Loading

0 comments on commit c24581d

Please sign in to comment.