@Transactional(propagation = Propagation.REQUIRED) public PaymentLog createNewPaymentLog(TransactionAdapter tx, TransactionStatus transactionStatus) throws MissingTransactionInformationException { try { return paymentLogService.save(new PaymentLog( tx.getTransactionId(), tx.getCurrencyType(), transactionStatus)); } catch (DataIntegrityViolationException e) { // The payment log was probably created by another instance just now. // This is not an error because the other instance will process the transaction. return null; } }
boolean pending = paymentLog.getTransactionStatus() == TransactionStatus.PENDING; boolean allocationMailSent = paymentLog.isAllocationMessageSent(); boolean refundEntryExists = paymentLog.getEligibleForRefund() == null; boolean noTokensAllocated = paymentLog.getAllocatedTomics() == null; boolean recentlyChanged = paymentLog.getProcessedDate().getTime() > new Date().getTime() - appConfig.getTransactionProcessingTime(); if (pending || allocationMailSent || (refundEntryExists && noTokensAllocated)
@Transactional(propagation = Propagation.REQUIRED) public PaymentLog sendAllocationMessageAndSavePaymentLog(PaymentLog paymentLog, BigDecimal amountInMainUnit, String transactionUrl) { messageService.send(new TokensAllocatedEmailMessage( MessageDTOHelper.build(paymentLog.getInvestor()), amountInMainUnit, paymentLog.getCurrency(), transactionUrl, convertTomicsToTokens(paymentLog.getAllocatedTomics()))); paymentLog.setAllocationMessageSent(true); return paymentLogService.updateProcessedDateAndSave(paymentLog); }
private PaymentLog createRefundEntryForAmount( PaymentLog paymentLog, RefundReason reason, BigInteger cryptocurrencyAmount, BigDecimal usdAmount) throws RefundEntryAlreadyExistsException { EligibleForRefund refund = eligibleForRefundService.save( new EligibleForRefund(reason, cryptocurrencyAmount, usdAmount, paymentLog.getCurrency(), paymentLog.getInvestor(), paymentLog.getTransactionId())); paymentLog.setEligibleForRefund(refund); return paymentLog; }
@Transactional(rollbackFor = Exception.class, propagation = Propagation.REQUIRED) public PaymentLog createRefundEntryForPaymentLogAndCommit( PaymentLog paymentLog, RefundReason reason) throws RefundEntryAlreadyExistsException { paymentLog = createRefundEntryForAmount(paymentLog, reason, paymentLog.getCryptocurrencyAmount(), paymentLog.getUsdAmount()); return paymentLogService.updateProcessedDateAndSave(paymentLog); }
PaymentLog allocateTokensWithRetries(PaymentLog paymentLog) throws Throwable { if (paymentLog.getUsdAmount() == null) { throw new IllegalArgumentException("PaymentLog's amount in USD must not be null."); } if (paymentLog.getBlockTime() == null) { throw new IllegalArgumentException("PaymentLog's block time must not be null."); } LOG.debug("Calling token allocation with {} USD for {} transaction {}.", paymentLog.getUsdAmount().toPlainString(), paymentLog.getCurrency(), paymentLog.getTransactionId()); try { // Retry as long as there are database locking exceptions. PaymentLog updatedPaymentLog = retryer.call( () -> monitorService.allocateTokens(paymentLog)); LOG.debug("Allocated {} tomics for {} transaction {}.", updatedPaymentLog.getAllocatedTomics(), updatedPaymentLog.getCurrency(), updatedPaymentLog.getTransactionId()); return updatedPaymentLog; } catch (Throwable e) { LOG.error("Failed to distribute payment to tiers for {} transaction {}.", paymentLog.getCurrency().name(), paymentLog.getTransactionId(), e.getCause()); RefundReason reason = RefundReason.TOKEN_ALLOCATION_FAILED; monitorService.createRefundEntryForPaymentLogAndCommit(paymentLog, reason); throw e; } }
/** * Prepares and converts a payment in the amount of tokenMax (from the given tier) divided by the given divisor. * Fails if the conversion throws a TokenAllocationResult. Asserts the returned amount of tokens. * Sets the amount of tokens that have to be sold on the given test tier for later assertions. */ private void makeAndConvertPaymentFailingOnOverflow(TestTier t, Date blockTime, int divisor) throws Throwable { BigInteger tomicsFromTier = t.getTomicsMax().divide(BigInteger.valueOf(divisor)); BigDecimal payment = monitorService.convertTomicsToUsd(tomicsFromTier, t.getDiscount()); t.tomicsSoldMustBe(t.tomicsSold.add(tomicsFromTier)); PaymentLog log = createPaymentLog(payment, blockTime, createInvestor()); log = monitorService.allocateTokens(log); if (log.getEligibleForRefund() != null) fail(); assertEquals(0, log.getAllocatedTomics().compareTo(tomicsFromTier)); }
if (paymentLog == null) return; // no processing required. if (isAmountInsufficient(paymentLog.getUsdAmount())) { monitorService.createRefundEntryForPaymentLogAndCommit(paymentLog, RefundReason.INSUFFICIENT_PAYMENT_AMOUNT); BigInteger allocatedTomics = paymentLog.getAllocatedTomics(); if (allocatedTomics == null) { paymentLog, tx.getTransactionValueInMainUnit(), tx.getTransactionUrl()); LOG.info("Transaction processed: {} {} / {} USD / {} FX / investor id {} / Time: {} / Tomics Amount {}", tx.getTransactionValueInMainUnit(), tx.getCurrencyType().name(), paymentLog.getUsdAmount(), paymentLog.getUsdFxRate(), tx.getAssociatedInvestor(), paymentLog.getCreateDate(), paymentLog.getAllocatedTomics()); "for that transaction.", paymentLog.getTransactionId(), e); } catch (Throwable t) { LOG.error("Error processing transaction.", t);
throws NoTierAtDateException { Date blockTime = paymentLog.getBlockTime(); BigDecimal usd = paymentLog.getUsdAmount(); Optional<SaleTier> oTier = saleTierService.getTierAtDate(blockTime); if (oTier.isPresent()) { result = distributeToTier(usd, oTier.get(), blockTime); paymentLog.setAllocatedTomics(result.getAllocatedTomics()); if (result.hasOverflow()) { LOG.info("{} transaction {} generated a overflow of {} USD", paymentLog.getCurrency().name(), paymentLog.getTransactionId(), result.getOverflow()); try { paymentLog = createRefundEntryForOverflow(paymentLog, result.getOverflow()); } catch (RefundEntryAlreadyExistsException e) { LOG.error("Couldn't save overflow USD {} as refund beacause a refund entry for " + "transaction {} already existed", result.getOverflow(), paymentLog.getTransactionId());
@Transactional(propagation = Propagation.REQUIRED) public PaymentLog sendTransactionReceivedMessageAndSavePaymentLog(PaymentLog paymentLog, BigDecimal amountInMainUnit, String transactionUrl) { messageService.send(new TransactionReceivedEmailMessage( MessageDTOHelper.build(paymentLog.getInvestor()), amountInMainUnit, paymentLog.getCurrency(), transactionUrl)); paymentLog.setTransactionReceivedMessageSent(true); return paymentLogService.updateProcessedDateAndSave(paymentLog); }
boolean confirmationMailSent = paymentLog.isTransactionReceivedMessageSent(); boolean notPending = paymentLog.getTransactionStatus() != TransactionStatus.PENDING; boolean recentlyChanged = paymentLog.getProcessedDate().getTime() > new Date().getTime() - appConfig.getTransactionProcessingTime(); if (confirmationMailSent || notPending || recentlyChanged) {
private PaymentLog createRefundEntryForOverflow(PaymentLog paymentLog, BigDecimal overflowInUsd) throws RefundEntryAlreadyExistsException { BigInteger cryptocurrencyAmount = overflowInUsd.multiply(paymentLog.getUsdFxRate()) .multiply(paymentLog.getCurrency().getAtomicUnitFactor()) .toBigInteger(); return createRefundEntryForAmount(paymentLog, RefundReason.TOKEN_OVERFLOW, cryptocurrencyAmount, overflowInUsd); }
@Test public void testSaveAndExistsAndFind() { Investor i = createInvestor(1); PaymentLog p = paymentLogRepository.save(createPaymentLog(i, "1")); // assertTrue(paymentLogRepository.existsByTxIdentifierAndCurrency("1", CURRENCY_TYPE)); Optional<PaymentLog> oPaymentLog = paymentLogRepository.findOptionalByTransactionId("1"); assertTrue(oPaymentLog.isPresent()); assertTrue(oPaymentLog.filter((paymentLog) -> paymentLog.getInvestor().getEmail().equals("emailAddress1") ).isPresent()); assertTrue(oPaymentLog.filter((paymentLog) -> paymentLog.equals(p)).isPresent()); }
@Test @Transactional(propagation = Propagation.NOT_SUPPORTED) public void testConcurrentPaymentsWithoutOverflow() throws InterruptedException { final Date blockTime = Date.valueOf("1970-01-02"); final TestTier t = new TestTier(1, "1970-01-01", "1970-01-03", new BigDecimal("0.25"), totalTomicsAmount().divide(new BigInteger("2")), true, false); int nrOfPayments = 10; BigDecimal singlePayment = BigDecimal.ONE; BigInteger singleSoldTomics = monitorService.convertUsdToTomics(singlePayment, t.getDiscount()).toBigInteger(); t.tomicsSoldMustBe(singleSoldTomics.multiply(BigInteger.valueOf(nrOfPayments))); Investor investor = createInvestor(); ThreadTestUtils.runMultiThread( () -> { try { PaymentLog log = createPaymentLog(singlePayment, blockTime, investor); log = ethereumMonitor.allocateTokensWithRetries(log); if (log.getEligibleForRefund() != null) fail(); assertEquals(0, log.getAllocatedTomics().compareTo(singleSoldTomics)); LOG.info("Distributed tomics: {}", log.getAllocatedTomics().toString()); } catch (Throwable throwable) { throwable.printStackTrace(); } }, nrOfPayments ); t.assertTier(); }
@Test @Transactional(propagation = Propagation.NOT_SUPPORTED) public void testDistributeToSingleTier() throws Throwable { // setup TestTier tt = new TestTier(1, "1970-01-01", "1970-01-03", new BigDecimal("0.2"), new BigInteger("1000").multiply(tomicsFactor()), true, false); Date blockTime = Date.valueOf("1970-01-02"); final BigInteger tomicsToSell = tt.getTomicsMax().divide(BigInteger.valueOf(2)); tt.tomicsSoldMustBe(tomicsToSell); final BigDecimal payment = monitorService.convertTomicsToUsd(tomicsToSell, tt.getDiscount()); // test PaymentLog log = createPaymentLog(payment, blockTime); log = monitorService.allocateTokens(log); if (log.getEligibleForRefund() != null) fail(); assertEquals(0, log.getAllocatedTomics().compareTo(tomicsToSell)); tt.assertTier(); }
private PaymentLog createPaymentLog(Investor investor, String txIdentifier) { return new PaymentLog( txIdentifier, new Date(), CURRENCY_TYPE, new Date(), new BigInteger("1"), new BigDecimal(2), new BigDecimal(3), investor, BigInteger.valueOf(100L), TransactionStatus.PENDING); }
@Test @Transactional(propagation = Propagation.NOT_SUPPORTED) public void testOverflowWithSingleTier() throws Throwable { // setup TestTier tt = new TestTier(1, "1970-01-01", "1970-01-03", new BigDecimal("0.25"), new BigInteger("1000").multiply(tomicsFactor()), true, false); Date blockTime = Date.valueOf("1970-01-02"); tt.newEndDateMustBe(blockTime); tt.mustBeFull(); final BigDecimal overflow = BigDecimal.TEN; final BigDecimal payment = monitorService.convertTomicsToUsd(tt.getTomicsMax(), tt.getDiscount()) .add(overflow); // test PaymentLog log = createPaymentLog(payment, blockTime); log = monitorService.allocateTokens(log); if (log.getEligibleForRefund() == null) fail(); // rounding the resulting USD overflow because that will be the actual precision with which the overflow // will be stored for refunds. assertEquals(0, log.getEligibleForRefund().getUsdAmount().round(new MathContext(6, RoundingMode.HALF_EVEN)).compareTo(overflow)); assertEquals(0, log.getAllocatedTomics().compareTo(tt.getTomicsMax())); tt.assertTier(); }
private PaymentLog createPaymentLog(BigDecimal usdAmount, Date blockTime, Investor investor) { String txId = "txId0"; java.util.Date creationDate = new java.util.Date(); CurrencyType currency = CurrencyType.ETH; // Value doesn't matter, only the amount in usd matters. BigInteger weiAmount = BigInteger.ONE; BigInteger tomicsAmount = null; PaymentLog paymentLog = null; boolean succeeded = false; int i = 1; while (!succeeded) { try { paymentLog = paymentLogService.saveTransactionless( new PaymentLog(txId, creationDate, currency, blockTime, weiAmount, USD_FX_RATE, usdAmount, investor, tomicsAmount, TransactionStatus.BUILDING)); succeeded = true; } catch (DataIntegrityViolationException ignore) { } txId = "txId" + i++; } return paymentLog; }
@Test @Transactional(propagation = Propagation.NOT_SUPPORTED) public void testOverflowTotalTokenAmountButNotTier() throws Throwable { // setup final Date blockTime = Date.valueOf("1970-01-02"); // tier with a capacity of total token amount plus one token. final TestTier tt1 = new TestTier(1, "1970-01-01", "1970-01-03", new BigDecimal("0.25"), totalTomicsAmount().add(tomicsFactor()), true, false); tt1.tomicsSoldMustBe(totalTomicsAmount()); final BigDecimal overflow = appConfig.getFiatBasePerToken().divide(new BigDecimal("2")); final BigDecimal payment = monitorService.convertTomicsToUsd(totalTomicsAmount(), tt1.getDiscount()).add(overflow); // test PaymentLog log = createPaymentLog(payment, blockTime); log = monitorService.allocateTokens(log); if (log.getEligibleForRefund() == null) fail(); assertEquals(0, log.getAllocatedTomics().compareTo(totalTomicsAmount())); assertEquals(0, log.getEligibleForRefund().getUsdAmount().round(new MathContext(6, RoundingMode.HALF_EVEN)).compareTo(overflow)); tt1.assertTier(); }
@Test @Transactional(propagation = Propagation.NOT_SUPPORTED) public void testOverflowTotalTokenAmountWithHalfFullTier() throws Throwable { final Date blockTime = Date.valueOf("1970-01-02"); // tier with a capacity of total token amount plus one token. final TestTier t = new TestTier(1, "1970-01-01", "1970-01-03", new BigDecimal("0.25"), totalTomicsAmount().add(tomicsFactor()), true, false); BigInteger tomicsFromTier = totalTomicsAmount().divide(new BigInteger("2")); BigDecimal payment = monitorService.convertTomicsToUsd(tomicsFromTier, t.getDiscount()); PaymentLog log = createPaymentLog(payment, blockTime); log = monitorService.allocateTokens(log); t.tomicsSoldMustBe(tomicsFromTier); if (log.getEligibleForRefund() != null) fail(); assertEquals(0, log.getAllocatedTomics().compareTo(tomicsFromTier)); t.assertTier(); final BigDecimal overflow = appConfig.getFiatBasePerToken().divide(new BigDecimal("2")); payment = monitorService.convertTomicsToUsd(tomicsFromTier, t.getDiscount()).add(overflow); t.tomicsSoldMustBe(totalTomicsAmount()); log = createPaymentLog(payment, blockTime); log = monitorService.allocateTokens(log); if (log.getEligibleForRefund() == null) fail(); assertEquals(0, log.getAllocatedTomics().compareTo(tomicsFromTier)); assertEquals(0, log.getEligibleForRefund().getUsdAmount().round(new MathContext(6, RoundingMode.HALF_EVEN)).compareTo(overflow)); t.assertTier(); }