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; } }
@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); }
boolean noTokensAllocated = paymentLog.getAllocatedTomics() == null; boolean recentlyChanged = paymentLog.getProcessedDate().getTime() > new Date().getTime() - appConfig.getTransactionProcessingTime();
@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(); }
/** * 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)); }
@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(); }
@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(); }
log = ethereumMonitor.allocateTokensWithRetries(log); if (log.getEligibleForRefund() != null) fail(); LOG.info("Distributed tomics: {}", log.getAllocatedTomics().toString()); } catch (Throwable throwable) { throwable.printStackTrace();
@Test @Transactional(propagation = Propagation.NOT_SUPPORTED) public void testOverflowTotalTokenAmountInSecondTier() throws Throwable { final BigInteger tomicsOverflowOverTier1 = tomicsFactor(); final Date blockTime = Date.valueOf("1970-01-02"); final TestTier tt1 = new TestTier(1, "1970-01-01", "1970-01-03", new BigDecimal("0.25"), totalTomicsAmount().subtract(tomicsOverflowOverTier1), true, false); tt1.mustBeFull(); tt1.newEndDateMustBe(blockTime); final TestTier tt2 = new TestTier(2, "1970-01-03", "1970-01-05", new BigDecimal("0.10"), totalTomicsAmount(), true, false); tt2.tomicsSoldMustBe(tomicsOverflowOverTier1); tt2.datesMustBeShiftedBy(tt1.getTier().getEndDate().getTime() - blockTime.getTime()); // payment setup final BigInteger tomicsToTier2 = tomicsOverflowOverTier1.multiply(BigInteger.valueOf(2)); final BigDecimal paymentToTier1 = monitorService.convertTomicsToUsd(tt1.getTomicsMax(), tt1.getDiscount()); final BigDecimal paymentToTier2 = monitorService.convertTomicsToUsd(tomicsToTier2, tt2.getDiscount()); final BigDecimal payment = paymentToTier1.add(paymentToTier2); final BigDecimal overflow = paymentToTier2.divide(new BigDecimal(2), new MathContext(6, RoundingMode.HALF_EVEN)); // 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(); tt2.assertTier(); }
/** * Two tiers, one payment that spills into the second tier. First tier has static duration behavior and therefore * doesn't change his end date nore the dates of the next tiers. */ @Test @Transactional(propagation = Propagation.NOT_SUPPORTED) public void testOverflowIntoSecondTierStaticDuration() throws Throwable { // setup Date blockTime = Date.valueOf("1970-01-02"); TestTier tt1 = new TestTier(1, "1970-01-01", "1970-01-03", new BigDecimal("0.25"), new BigInteger("1000").multiply(tomicsFactor()), false, false); final BigInteger tomicsToSellFromTier1 = tt1.getTomicsMax(); tt1.mustBeFull(); tt1.tomicsSoldMustBe(tomicsToSellFromTier1); TestTier tt2 = new TestTier(2, "1970-01-03", "1970-01-05", new BigDecimal("0.1"), new BigInteger("1000").multiply(tomicsFactor()), false, false); final BigInteger tomicsToSellFromTier2 = tt2.getTomicsMax().divide(BigInteger.valueOf(3)); tt2.tomicsSoldMustBe(tomicsToSellFromTier2); final BigDecimal paymentToTier1 = monitorService.convertTomicsToUsd(tomicsToSellFromTier1, tt1.getDiscount()); final BigDecimal paymentToTier2 = monitorService.convertTomicsToUsd(tomicsToSellFromTier2, tt2.getDiscount()); BigDecimal payment = paymentToTier1.add(paymentToTier2); // test PaymentLog log = createPaymentLog(payment, blockTime); log = monitorService.allocateTokens(log); if (log.getEligibleForRefund() != null) fail(); assertEquals(0, log.getAllocatedTomics().compareTo(tomicsToSellFromTier1.add(tomicsToSellFromTier2))); tt1.assertTier(); tt2.assertTier(); makeAndConvertPaymentFailingOnOverflow(tt2, blockTime, 3); tt1.assertTier(); tt2.assertTier(); }
BigInteger allocatedTomics = paymentLog.getAllocatedTomics(); if (allocatedTomics == null) { tx.getTransactionValueInMainUnit(), tx.getCurrencyType().name(), paymentLog.getUsdAmount(), paymentLog.getUsdFxRate(), tx.getAssociatedInvestor(), paymentLog.getCreateDate(), paymentLog.getAllocatedTomics());
assertEquals(0, tomicsFromTier1.compareTo(log.getAllocatedTomics())); if (log.getEligibleForRefund() != null) fail(); tiers.forEach(TestTier::assertTier); log = createPaymentLog(paymentToTier3, blockTime); log = monitorService.allocateTokens(log); assertEquals(0, tomicsFromTier3.compareTo(log.getAllocatedTomics())); if (log.getEligibleForRefund() != null) fail(); tiers.forEach(TestTier::assertTier); assertEquals(0, overallRemainingTomics.compareTo(log.getAllocatedTomics())); if (log.getEligibleForRefund() == null) fail(); assertEquals(0, log.getAllocatedTomics().compareTo(overallRemainingTomics)); assertEquals(0, log.getEligibleForRefund().getUsdAmount().round(new MathContext(6, RoundingMode.HALF_EVEN)).compareTo(BigDecimal.TEN)); tiers.forEach(TestTier::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(); }
log = monitorService.allocateTokens(log); if (log.getEligibleForRefund() != null) fail(); assertEquals(0, log.getAllocatedTomics().compareTo(tomicsToSellFromTier1.add(tomicsToSellFromTier2))); tt1.assertTier(); tt2.assertTier();
log = monitorService.allocateTokens(log); if (log.getEligibleForRefund() == null) fail(); assertEquals(0, log.getAllocatedTomics().compareTo(tt1.getTomicsMax().add(tt2.getTomicsMax()).add(tt3.getTomicsMax()))); assertEquals(0, log.getEligibleForRefund().getUsdAmount().round(new MathContext(6, RoundingMode.HALF_EVEN)).compareTo(overflow)); tt1.assertTier(); log = monitorService.allocateTokens(log); if (log.getEligibleForRefund() == null) fail(); assertEquals(0, log.getAllocatedTomics().compareTo(BigInteger.ZERO)); assertEquals(0, log.getEligibleForRefund().getUsdAmount().compareTo(BigDecimal.TEN));
log = monitorService.allocateTokens(log); if (log.getEligibleForRefund() != null) fail(); assertEquals(0, log.getAllocatedTomics().compareTo(tomicsToSellFromTier1.add(tomicsToSellFromTier2))); tt1.assertTier(); tt2.assertTier();
@Test @Transactional(propagation = Propagation.NOT_SUPPORTED) public void testOverflowTotalTokenAmountAndTier() 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()); // only the total amount of tokens can be disttributed/converted. final BigDecimal usdAmountConverted = monitorService.convertTomicsToUsd(totalTomicsAmount(), tt1.getDiscount()); final BigDecimal overflowOverTier = BigDecimal.TEN; final BigDecimal payment = monitorService.convertTomicsToUsd(tt1.getTomicsMax(), tt1.getDiscount()) .add(overflowOverTier); final BigDecimal overflow = payment.subtract(usdAmountConverted); // 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(); }