Browse Source

эмиттер уже в монге!

kpmy 5 years ago
parent
commit
aeded32ecc

+ 5 - 0
db/2020-05/views.js

@@ -0,0 +1,5 @@
+db.coin.aggregate([
+    {$match: {status: 'indoor'}},
+    {$group: {_id: "overall", amount: {$sum: "$coin.amount"}, count: {$sum: 1}}}
+], {allowDiskUse: true}).saveAsView("coin-overall-view")
+

+ 3 - 1
src/main/java/inn/ocsf/bee/freigeld/core/model/Coin.java

@@ -11,5 +11,7 @@ public interface Coin {
         return getValue().getAmount() - (getEra() * getValue().getDelta());
         return getValue().getAmount() - (getEra() * getValue().getDelta());
     }
     }
 
 
-    Integer getEra();
+    int getEra();
+
+    void incEra();
 }
 }

+ 46 - 0
src/main/java/inn/ocsf/bee/freigeld/core/model/CoinDescr.java

@@ -0,0 +1,46 @@
+package inn.ocsf.bee.freigeld.core.model;
+
+import java.util.UUID;
+
+public class CoinDescr implements Coin {
+
+    CoinValue value;
+
+    int era;
+
+    public CoinDescr(CoinValue value, int era) {
+        this.value = value;
+        this.era = era;
+    }
+
+    public CoinDescr() {
+    }
+
+    @Override
+    public UUID getId() {
+        return null;
+    }
+
+    @Override
+    public CoinValue getValue() {
+        return value;
+    }
+
+    public void setValue(CoinValue value) {
+        this.value = value;
+    }
+
+    @Override
+    public int getEra() {
+        return era;
+    }
+
+    public void setEra(int era) {
+        this.era = era;
+    }
+
+    @Override
+    public void incEra() {
+        this.era = Long.valueOf(Math.min(CoinValue.MAX_ERA, this.era + 1)).intValue();
+    }
+}

+ 106 - 0
src/main/java/inn/ocsf/bee/freigeld/core/model/data/CoinData.java

@@ -1,15 +1,28 @@
 package inn.ocsf.bee.freigeld.core.model.data;
 package inn.ocsf.bee.freigeld.core.model.data;
 
 
+import com.querydsl.core.annotations.QueryEntity;
+import inn.ocsf.bee.freigeld.core.model.Coin;
+import inn.ocsf.bee.freigeld.core.model.CoinValue;
 import org.bson.types.ObjectId;
 import org.bson.types.ObjectId;
 import org.springframework.data.annotation.Id;
 import org.springframework.data.annotation.Id;
+import org.springframework.data.annotation.Version;
 import org.springframework.data.mongodb.core.mapping.Document;
 import org.springframework.data.mongodb.core.mapping.Document;
 
 
+import java.util.UUID;
+
 @Document("coin")
 @Document("coin")
 public class CoinData {
 public class CoinData {
 
 
     @Id
     @Id
     private ObjectId id;
     private ObjectId id;
 
 
+    private CoinImpl coin;
+
+    private CoinStatus status;
+
+    @Version
+    private Long version;
+
     public ObjectId getId() {
     public ObjectId getId() {
         return id;
         return id;
     }
     }
@@ -17,4 +30,97 @@ public class CoinData {
     public void setId(ObjectId id) {
     public void setId(ObjectId id) {
         this.id = id;
         this.id = id;
     }
     }
+
+    public CoinStatus getStatus() {
+        return status;
+    }
+
+    public void setStatus(CoinStatus status) {
+        this.status = status;
+    }
+
+    public Coin getCoin() {
+        return coin;
+    }
+
+    public void setCoin(Coin coin) {
+        if (coin instanceof CoinImpl) {
+            this.coin = (CoinImpl) coin;
+        } else if (coin != null) {
+            throw new RuntimeException("not suppoted coin impl");
+        } else {
+            this.coin = null;
+        }
+    }
+
+    @QueryEntity
+    public static class CoinImpl implements Coin {
+
+        private UUID id;
+
+        private int era;
+
+        private CoinValue value;
+
+        private Long amount;
+
+        public CoinImpl() {
+        }
+
+        public CoinImpl(CoinValue value, int era) {
+            this.id = UUID.randomUUID();
+            this.era = era;
+            this.value = value;
+            this.amount = getCurrent();
+        }
+
+        public CoinImpl(UUID id, int era, CoinValue value) {
+            this.id = id;
+            this.era = era;
+            this.value = value;
+            this.amount = getCurrent();
+        }
+
+        @Override
+        public UUID getId() {
+            return id;
+        }
+
+        public void setId(UUID id) {
+            this.id = id;
+        }
+
+        @Override
+        public int getEra() {
+            return era;
+        }
+
+        public void setEra(int era) {
+            this.era = era;
+            this.amount = getCurrent();
+        }
+
+        @Override
+        public void incEra() {
+            this.era = Long.valueOf(Math.min(CoinValue.MAX_ERA, this.era + 1)).intValue();
+            this.amount = getCurrent();
+        }
+
+        @Override
+        public CoinValue getValue() {
+            return value;
+        }
+
+        public void setValue(CoinValue value) {
+            this.value = value;
+            this.amount = getCurrent();
+        }
+
+        public Long getAmount() {
+            if (amount == null) {
+                amount = getCurrent();
+            }
+            return amount;
+        }
+    }
 }
 }

+ 5 - 0
src/main/java/inn/ocsf/bee/freigeld/core/model/data/CoinStatus.java

@@ -0,0 +1,5 @@
+package inn.ocsf.bee.freigeld.core.model.data;
+
+public enum CoinStatus {
+    indoor, outdoor, free, nil
+}

+ 10 - 1
src/main/java/inn/ocsf/bee/freigeld/core/repo/CoinRepository.java

@@ -1,9 +1,18 @@
 package inn.ocsf.bee.freigeld.core.repo;
 package inn.ocsf.bee.freigeld.core.repo;
 
 
 import inn.ocsf.bee.freigeld.core.model.data.CoinData;
 import inn.ocsf.bee.freigeld.core.model.data.CoinData;
+import inn.ocsf.bee.freigeld.core.model.data.CoinStatus;
 import org.bson.types.ObjectId;
 import org.bson.types.ObjectId;
 import org.springframework.data.mongodb.repository.MongoRepository;
 import org.springframework.data.mongodb.repository.MongoRepository;
 import org.springframework.data.querydsl.QuerydslPredicateExecutor;
 import org.springframework.data.querydsl.QuerydslPredicateExecutor;
 
 
-public interface CoinRepository extends MongoRepository<CoinData, ObjectId>, QuerydslPredicateExecutor<CoinData> {
+import java.util.Optional;
+import java.util.UUID;
+import java.util.stream.Stream;
+
+public interface CoinRepository extends MongoRepository<CoinData, ObjectId>, QuerydslPredicateExecutor<CoinData>, CoinRepositoryCustom {
+
+    Optional<CoinData> findOneByCoinId(UUID coin_id);
+
+    Stream<CoinData> findAllByStatus(CoinStatus status);
 }
 }

+ 14 - 0
src/main/java/inn/ocsf/bee/freigeld/core/repo/CoinRepositoryCustom.java

@@ -0,0 +1,14 @@
+package inn.ocsf.bee.freigeld.core.repo;
+
+import inn.ocsf.bee.freigeld.core.model.CoinValue;
+import inn.ocsf.bee.freigeld.core.model.data.CoinData;
+import org.jetbrains.annotations.NotNull;
+
+public interface CoinRepositoryCustom {
+
+    @NotNull
+    CoinData create(@NotNull CoinValue value, @NotNull Integer era);
+
+    @NotNull
+    Long getOverall();
+}

+ 243 - 0
src/main/kotlin/inn/ocsf/bee/freigeld/core/calc/CoinUtils.kt

@@ -0,0 +1,243 @@
+package inn.ocsf.bee.freigeld.core.calc
+
+import inn.ocsf.bee.freigeld.core.model.Coin
+import inn.ocsf.bee.freigeld.core.model.CoinDescr
+import inn.ocsf.bee.freigeld.core.model.CoinDescrInv
+import inn.ocsf.bee.freigeld.core.model.CoinValue
+import java.util.*
+
+class CoinUtils {
+
+    companion object {
+
+        val maxEraValue: Int = CoinValue.MAX_ERA.toInt()
+
+        val avaiues = CoinValue.values().flatMap { value -> (0..maxEraValue - 1).map { era -> value to era to (value.getCurrent(era)) } }.map { it.second to it.first }.groupBy { it.first }.map { it.key to it.value.map { it.second } }.toMap()
+        val akeis = avaiues.keys.sortedDescending()
+
+        private fun makeSomeFake(cashback: Long): Collection<Coin> {
+            val ret = mutableListOf<Coin>()
+            var totalCashback = cashback
+            do {
+                val value = CoinValue.values().filter { it.amount >= Math.abs(totalCashback) }.sortedBy { it.amount }.first()
+                val valueByEra = (0..(maxEraValue - 1)).map { era -> era to (value.amount - (era * value.delta)) }.filter { it.second <= Math.abs(totalCashback) }.firstOrNull()
+                if (valueByEra != null) {
+                    ret.add(CoinDescrInv(value, -valueByEra.second, valueByEra.first))
+                    totalCashback += valueByEra.second
+                } else {
+                    throw RuntimeException("no value")
+                }
+            } while (totalCashback < 0)
+            return ret
+        }
+
+        private fun splitCoins(coins: Collection<Coin>, amount: Long): Collection<Coin> {
+            val ret = mutableListOf<Coin>()
+            val coinMap = coins.groupBy { it.current }.map { it.key to it.value.map { it.id }.toMutableList() }.toMap().toMutableMap()
+            val usedCoins = mutableSetOf<UUID>()
+            var totalAmount = 0L
+            do {
+                val diffAmount = amount - totalAmount
+                val leastCoinMap = coinMap.map { it.key to it.value.subtract(usedCoins).toList() }.filter { it.second.isNotEmpty() }.toMap()
+                val loeCoinMap = leastCoinMap.filter { it.key <= diffAmount }.map { it.key to it.value }.sortedByDescending { it.first }
+                if (loeCoinMap.isNotEmpty()) {
+                    val biggestLoe = loeCoinMap.first().first
+                    totalAmount += biggestLoe
+                    val coinId = loeCoinMap.first().second.first()
+                    usedCoins.add(coinId)
+                } else {
+                    val smallestGt = leastCoinMap.keys.sorted().firstOrNull()
+                    if (smallestGt != null) {
+                        val cashbackAmount = if (smallestGt >= amount && smallestGt > totalAmount) {
+                            usedCoins.clear() //одной монетой перекрываем всё предыдущее
+                            totalAmount = smallestGt
+                            val coinId = leastCoinMap.get(smallestGt)?.first()!!
+                            usedCoins.add(coinId)
+                            amount - smallestGt
+                        } else {
+                            totalAmount += smallestGt
+                            val coinId = leastCoinMap.get(smallestGt)?.first()!!
+                            usedCoins.add(coinId)
+                            diffAmount - smallestGt
+                        }
+                        ret.addAll(makeSomeFake(cashbackAmount))
+                    } else {
+                        TODO()
+                    }
+                }
+            } while (totalAmount < amount)
+            ret.addAll(coins.filter { usedCoins.contains(it.id) })
+            val realAmount = ret.filter { it.id != null }.map { it.current }.sum()
+            if (totalAmount != realAmount) {
+                TODO()
+            }
+            val fakeAmount = ret.map { it.current }.sum()
+            if (fakeAmount != amount) {
+                TODO()
+            }
+            return ret
+        }
+
+        fun sum(coins: Iterable<Coin>): Long = coins.map { it.current }.sum()
+
+        fun sumString(coins: Iterable<Coin>): String {
+            val pos = sum(coins.filter { it.id != null })
+            val neg = sum(coins.filter { it.id == null })
+            return if (neg != 0L) "${pos + neg}(${pos}${neg})" else "$pos"
+        }
+
+        fun makeSomeFuture(x: Long): List<Coin> {
+            return makeSomeVariants(x).first().flatMap { n -> (0 until n.value).map { CoinDescr(n.key.first, n.key.second) } }
+        }
+
+        fun makeSomeStrategy(st: CoinStrategy): CoinStrategy {
+            return if (st.credit > 0) {
+                if (st.leftCoins != null) {
+                    if (st.credit <= sum(st.leftCoins)) {
+                        val creditCoins = splitCoins(st.leftCoins, st.credit)
+                        val creditCoinsReal = creditCoins.filter { it.id != null }
+                        val creditCoinsCashback = creditCoins.filter { it.id == null }
+                        if (creditCoinsCashback.isNotEmpty()) {
+                            val creditCashback = sum(creditCoinsReal) - st.credit
+                            if (st.rightCoins != null && creditCashback <= sum(st.rightCoins)) {
+                                if (st.leftCoins.map { it.id }.toSet().intersect(st.rightCoins.map { it.id }.toSet()).isNotEmpty()) {
+                                    throw RuntimeException("same coins, alarma")
+                                }
+                                val debitCoins = splitCoins(st.rightCoins, creditCashback)
+                                val debitCoinsReal = debitCoins.filter { it.id != null }
+                                val debitCoinsCashback = debitCoins.filter { it.id == null }
+                                if (debitCoinsCashback.isNotEmpty()) {
+                                    val debitCashback = sum(debitCoinsReal) - creditCashback
+                                    val replaceStrategy: CoinStrategy? = if (st.parent == null) {
+                                        val debitNoCashbackStrategy = CoinStrategy(st.credit + debitCashback, st.leftCoins, st.rightCoins)
+                                        debitNoCashbackStrategy.parent = st
+                                        makeSomeStrategy(debitNoCashbackStrategy)
+                                        if (setOf(CoinStrategyType.L2R_R, CoinStrategyType.L2R).contains(debitNoCashbackStrategy.res)) {
+                                            null //TODO exchange with extra, L3R_R
+                                        } else {
+                                            null
+                                        }
+                                    } else {
+                                        null
+                                    }
+                                    if (replaceStrategy != null) {
+                                        replaceStrategy
+                                    } else {
+                                        st.leftExtract = creditCoinsReal
+                                        st.leftEmit = makeSomeFuture(creditCashback)
+                                        st.rightExtract = debitCoinsReal
+                                        st.rightEmit = makeSomeFuture(debitCashback)
+                                        st.rightCashback = st.leftExtract
+                                        st.res = CoinStrategyType.L2R_RL
+                                        st
+                                    }
+                                } else {
+                                    st.leftExtract = creditCoinsReal
+                                    st.rightExtract = debitCoinsReal
+                                    st.leftCashback = st.rightExtract
+                                    st.rightCashback = st.leftExtract
+                                    st.res = CoinStrategyType.L2R_R
+                                    st
+                                }
+                            } else if (st.rightCoins == null) {
+                                st.leftExtract = creditCoinsReal
+                                st.leftEmit = makeSomeFuture(creditCashback)
+                                st.rightEmit = makeSomeFuture(st.credit)
+                                st.res = CoinStrategyType.B2R_E
+                                st
+                            } else {
+                                st.leftExtract = creditCoinsReal
+                                st.leftEmit = makeSomeFuture(creditCashback)
+                                st.rightEmit = makeSomeFuture(st.credit)
+                                st.res = CoinStrategyType.L2R_E
+                                st
+                            }
+                        } else {
+                            st.leftExtract = creditCoins
+                            st.rightCashback = creditCoins
+                            st.res = CoinStrategyType.L2R
+                            st
+                        }
+                    } else {
+                        st.res = CoinStrategyType.ERR_LC
+                        st
+                    }
+                } else {
+                    st.rightEmit = makeSomeFuture(st.credit)
+                    st.res = CoinStrategyType.E2B
+                    st
+                }
+            } else {
+                st.res = CoinStrategyType.ERR_NUL
+                st
+            }
+        }
+
+        fun makeSomeVariants(x: Long, skip: Set<Long>? = null): List<Map<Pair<CoinValue, Int>, Long>> {
+            val thisAkeis = if (skip != null && skip.isNotEmpty()) {
+                akeis.filterNot { skip.contains(it) }
+            } else {
+                akeis
+            }
+            if (!thisAkeis.contains(1)) throw IllegalArgumentException("cannot split without 1")
+            val rkeys = mutableListOf<Pair<Long, Long>>()
+            var z = x
+            do {
+                val next = thisAkeis.first { z >= it }
+                val n = z / next
+                rkeys.add(next to n)
+                z %= (n * next)
+            } while (z != 0L)
+
+            val ret = mutableListOf<Map<Pair<CoinValue, Int>, Long>>()
+            val rmap = rkeys.map { it.first to avaiues.get(it.first)?.toSet()!! }
+
+            var calcFn: ((Int, List<Pair<CoinValue, Int>>) -> Unit)? = null
+
+            calcFn = { depth, keys ->
+                if (depth < rmap.size) {
+                    rmap[depth].second.forEach { k ->
+                        calcFn!!(depth + 1, keys + listOf(k))
+                    }
+                } else if (depth == rmap.size) {
+                    calcFn!!(depth + 1, keys)
+                } else {
+                    ret.add(keys.mapIndexed { i, k -> k to rkeys[i].second }.toMap())
+                }
+            }
+
+            calcFn(0, listOf())
+
+            return if (x > 0L) {
+                ret
+            } else {
+                listOf()
+            }
+        }
+    }
+}
+
+
+enum class CoinStrategyType {
+    L2R_RL, L2R_R, B2R_E, L2R, E2B, L2R_E, ERR_LC, ERR_NUL;
+
+    companion object {
+        val OK = setOf(L2R_RL, L2R_R, B2R_E, L2R, E2B, L2R_E)
+        val ERR = setOf(ERR_LC, ERR_NUL)
+    }
+
+    fun isOk(): Boolean = OK.contains(this)
+
+    fun isErr(): Boolean = ERR.contains(this)
+}
+
+class CoinStrategy(val credit: Long, val leftCoins: Collection<Coin>? = null, val rightCoins: Collection<Coin>? = null) {
+    var res: CoinStrategyType? = null
+    var parent: CoinStrategy? = null
+    var leftExtract: Collection<Coin>? = null
+    var rightExtract: Collection<Coin>? = null
+    var leftCashback: Collection<Coin>? = null
+    var rightCashback: Collection<Coin>? = null
+    var leftEmit: Collection<Coin>? = null
+    var rightEmit: Collection<Coin>? = null
+}

+ 144 - 0
src/main/kotlin/inn/ocsf/bee/freigeld/core/data/GlobalEmitterImpl.kt

@@ -0,0 +1,144 @@
+package inn.ocsf.bee.freigeld.core.data
+
+import com.querydsl.core.types.dsl.Expressions
+import inn.ocsf.bee.freigeld.core.calc.CoinUtils
+import inn.ocsf.bee.freigeld.core.model.*
+import inn.ocsf.bee.freigeld.core.model.data.CoinData
+import inn.ocsf.bee.freigeld.core.model.data.CoinStatus
+import inn.ocsf.bee.freigeld.core.model.data.QCoinData.coinData
+import inn.ocsf.bee.freigeld.core.repo.CoinRepository
+import kotlinx.coroutines.CompletableDeferred
+import kotlinx.coroutines.GlobalScope
+import kotlinx.coroutines.launch
+import org.slf4j.LoggerFactory
+import org.springframework.stereotype.Service
+import java.util.*
+import java.util.concurrent.CompletableFuture
+import java.util.concurrent.locks.ReentrantLock
+import java.util.function.Consumer
+import javax.annotation.PostConstruct
+import javax.inject.Inject
+import kotlin.concurrent.withLock
+
+@Service
+class GlobalEmitterImpl : GlobalEmitter {
+
+    @Inject
+    private lateinit var coinRepo: CoinRepository
+
+    private val globalId = UUID.fromString("a671cdca-782a-4caf-8aad-056f6b62d822")
+
+    val listeners = mutableListOf<Consumer<EmitterEvent>>()
+
+    val deferredLock = ReentrantLock()
+
+    private val log = LoggerFactory.getLogger(javaClass)
+
+    @PostConstruct
+    fun init() {
+        coinRepo.deleteAll()
+    }
+
+    private fun computeIfAbsent(future: Coin? = null, fn: () -> CoinData): Coin {
+        var ret: Coin? = if (future != null) {
+            coinRepo.findAll(Expressions.allOf(coinData.status.eq(CoinStatus.indoor), coinData.coin.value.eq(future.value), coinData.coin.era.eq(future.era))).map { it.coin }.firstOrNull()
+        } else {
+            null
+        }
+        return if (ret == null) {
+            var coin = fn()
+            coin = coinRepo.save(coin)
+            coin.coin
+        } else {
+            log.info("emitter reuse coin ${CoinUtils.sum(listOf(ret))}")
+            ret
+        }
+    }
+
+    override fun listen(fn: Consumer<EmitterEvent>) {
+        listeners.add(fn)
+    }
+
+    private suspend fun calcLater(): Set<UUID> {
+        val ret = mutableSetOf<UUID>()
+        coinRepo.findAllByStatus(CoinStatus.free).peek {
+            it.coin.incEra()
+            if (it.coin.era == CoinValue.MAX_ERA.toInt()) {
+                it.status = CoinStatus.nil
+                ret.add(it.coin.id)
+            }
+        }.forEach { coinRepo.save(it) }
+        return ret
+    }
+
+    override fun calc(): CompletableFuture<Any> {
+        val ret = CompletableFuture<Any>()
+        listeners.forEach { it.accept(EmitterStartRecalculationEvent()) }
+        deferredLock.withLock {
+            val deferred = CompletableDeferred<Boolean>()
+            deferred.invokeOnCompletion { t: Throwable? -> if (t == null) ret.complete(null) else ret.completeExceptionally(t) }
+            GlobalScope.launch {
+                try {
+                    val nullCoins = calcLater()
+                    deferred.complete(true)
+                    listeners.forEach { it.accept(EmitterStopRecalculationEvent(nullCoins)) }
+                } catch (t: Throwable) {
+                    deferred.completeExceptionally(t)
+                }
+            }
+        }
+        return ret
+    }
+
+    override fun emitOne(coin: Coin): UUID {
+        return computeIfAbsent(coin) {
+            val new = coinRepo.create(coin.value, coin.era)
+            log.info("emitter emit coin ${CoinUtils.sum(listOf(new.coin))}")
+            new
+        }.id
+    }
+
+    override fun emit(count: Long, value: CoinValue) {
+        (0 until count).forEach {
+            computeIfAbsent { coinRepo.create(value, 0) }
+        }
+        log.info("emitter emit ${count} of ${value}")
+    }
+
+    override fun getOverall(): Long {
+        return coinRepo.overall
+    }
+
+    override fun getCoins(): MutableCollection<Coin>? {
+        return null
+    }
+
+    override fun getId(): UUID {
+        return globalId
+    }
+
+    private fun setStatus(coinId: UUID, status: CoinStatus, expectStatus: Set<CoinStatus>? = null): CoinData {
+        val coin = coinRepo.findOneByCoinId(coinId).orElseThrow { IllegalArgumentException("coin ${coinId} must be real") }
+        if (coin.status != status && expectStatus != null && expectStatus.isNotEmpty() && !expectStatus.contains(coin.status)) {
+            throw RuntimeException("unexpected old status")
+        }
+        return if (coin.status != status && coin.status != CoinStatus.nil) {
+            coin.status = status
+            coinRepo.save(coin)
+        } else {
+            coin
+        }
+    }
+
+    override fun acceptOne(coin: Coin) {
+        setStatus(coin.id, CoinStatus.indoor, setOf(CoinStatus.nil, CoinStatus.free, CoinStatus.outdoor))
+    }
+
+    override fun free(c: Coin) {
+        setStatus(c.id, CoinStatus.free, setOf(CoinStatus.indoor, CoinStatus.outdoor))
+    }
+
+    override fun extractOne(coinId: UUID): Coin? {
+        return setStatus(coinId, CoinStatus.outdoor, setOf(CoinStatus.indoor)).coin
+    }
+}

+ 6 - 433
src/main/kotlin/inn/ocsf/bee/freigeld/core/demo/DemoInMem.kt

@@ -1,11 +1,10 @@
 package inn.ocsf.bee.freigeld.core.demo
 package inn.ocsf.bee.freigeld.core.demo
 
 
-import GlobalEmitter
 import com.oblac.nomen.Nomen
 import com.oblac.nomen.Nomen
+import inn.ocsf.bee.freigeld.core.calc.CoinStrategy
+import inn.ocsf.bee.freigeld.core.calc.CoinUtils
 import inn.ocsf.bee.freigeld.core.model.*
 import inn.ocsf.bee.freigeld.core.model.*
 import kotlinx.coroutines.CompletableDeferred
 import kotlinx.coroutines.CompletableDeferred
-import kotlinx.coroutines.GlobalScope
-import kotlinx.coroutines.launch
 import kotlinx.coroutines.runBlocking
 import kotlinx.coroutines.runBlocking
 import org.slf4j.LoggerFactory
 import org.slf4j.LoggerFactory
 import org.springframework.beans.factory.annotation.Autowired
 import org.springframework.beans.factory.annotation.Autowired
@@ -13,10 +12,8 @@ import org.springframework.scheduling.annotation.Scheduled
 import org.springframework.stereotype.Service
 import org.springframework.stereotype.Service
 import java.util.*
 import java.util.*
 import java.util.concurrent.CompletableFuture
 import java.util.concurrent.CompletableFuture
-import java.util.function.Consumer
 import javax.annotation.PostConstruct
 import javax.annotation.PostConstruct
 import kotlin.math.absoluteValue
 import kotlin.math.absoluteValue
-import kotlin.math.min
 import kotlin.math.roundToLong
 import kotlin.math.roundToLong
 
 
 @Service
 @Service
@@ -300,7 +297,10 @@ class GlobalBank : Bank {
                 }
                 }
                 is EmitterStopRecalculationEvent -> {
                 is EmitterStopRecalculationEvent -> {
                     accountMap.values.map { it to it.coins?.map { it.id }?.intersect(e.nullCoins) }.filter { it.second?.isNotEmpty() ?: false }.forEach { ap ->
                     accountMap.values.map { it to it.coins?.map { it.id }?.intersect(e.nullCoins) }.filter { it.second?.isNotEmpty() ?: false }.forEach { ap ->
-                        val nullCoins = ap.second?.map { ap.first.extractOne(it) }?.toSet() ?: setOf()
+                        val nullCoins = ap.second?.map {
+                            //(ap.first as DemoAccount).internalCoins.get(it)?.incEra() //TODO убрать!
+                            ap.first.extractOne(it)
+                        }?.toSet() ?: setOf()
                         emitter.accept(nullCoins)
                         emitter.accept(nullCoins)
                     }
                     }
                     globalQueue.filter { it is BankPauseOnRecalcEvent }.map { it as BankPauseOnRecalcEvent }.first().paused = false
                     globalQueue.filter { it is BankPauseOnRecalcEvent }.map { it as BankPauseOnRecalcEvent }.first().paused = false
@@ -318,7 +318,6 @@ class GlobalBank : Bank {
                 globalQueue.remove(e)
                 globalQueue.remove(e)
             }
             }
             pollCount++
             pollCount++
-            log.info("poll")
         }
         }
     }
     }
 
 
@@ -394,199 +393,6 @@ class ExchangeStartEvent(eventId: UUID, val to: BankAccount, val from: BankAccou
 
 
 class BankPauseOnRecalcEvent(eventId: UUID, val emitterEvent: EmitterStartRecalculationEvent, var paused: Boolean = true) : AbstractBankEvent(eventId)
 class BankPauseOnRecalcEvent(eventId: UUID, val emitterEvent: EmitterStartRecalculationEvent, var paused: Boolean = true) : AbstractBankEvent(eventId)
 
 
-@Service
-class DemoInMemEmitter : GlobalEmitter {
-
-    val globalId = UUID.fromString("a671cdca-782a-4caf-8aad-056f6b62d822")
-    val coinMap = mutableMapOf<UUID, Coin>()
-    val coinValueIndex = mutableMapOf<Pair<CoinValue, Int>, MutableSet<UUID>>()
-    val coinExtractedSet = mutableSetOf<UUID>()
-    val coinFreeSet = mutableSetOf<UUID>()
-    val listeners = mutableListOf<Consumer<EmitterEvent>>()
-    var deferred: CompletableDeferred<Any>? = null
-
-    private val log = LoggerFactory.getLogger(javaClass)
-
-    private fun computeIfAbsent(future: Coin? = null, fn: (UUID) -> DemoCoin): Coin {
-        var ret: Coin? = if (future != null) {
-            coinValueIndex.get(future.value to future.era)?.filterNot { coinExtractedSet.contains(it) }?.firstOrNull()?.let { coinMap.get(it) }
-        } else {
-            null
-        }
-        return if (ret == null) {
-            val coin = coinMap.computeIfAbsent(UUID.randomUUID(), fn)
-            coinValueIndex.computeIfAbsent(coin.value to coin.era) { c -> mutableSetOf() }.add(coin.id)
-            coin
-        } else {
-            log.info("emitter reuse coin ${CoinUtils.sum(listOf(ret))}")
-            ret
-        }
-    }
-
-    override fun emit(count: Long, value: CoinValue) {
-        (0 until count).forEach {
-            computeIfAbsent { id ->
-                val coin = DemoCoin(id, value)
-                coin
-            }
-        }
-        log.info("emitter emit ${count} of ${value}")
-    }
-
-    override fun acceptOne(nullCoin: Coin) {
-        coinFreeSet.remove(nullCoin.id)
-        coinExtractedSet.remove(nullCoin.id)
-        coinValueIndex.computeIfAbsent(nullCoin.value to nullCoin.era) { c -> mutableSetOf() }.add(nullCoin.id)
-        log.info("emitter redeem coin ${CoinUtils.sum(listOf(nullCoin))}")
-    }
-
-    override fun getId(): UUID {
-        return globalId
-    }
-
-    override fun extractOne(coinId: UUID): Coin {
-        val coin = coinMap.get(coinId)!!
-        if (!coinExtractedSet.add(coin.id)) {
-            throw RuntimeException("coin already extracted")
-        }
-        coinValueIndex.computeIfAbsent(coin.value to coin.era) { c -> mutableSetOf() }.remove(coin.id)
-        log.info("emitter extract ${CoinUtils.sum(listOf(coin))}, ${CoinUtils.sumString(listOf(coin))}")
-        return coin
-    }
-
-    /*override fun extractMoreOrExact(amount: Long): MutableCollection<Coin> {
-        val indoorCoins = coinMap.values.filter { !coinExtractedSet.contains(it.id) }
-        val ret = CoinUtils.mergeCoins(indoorCoins, amount)
-        val realCoins = ret.filter { it.id != null }
-        realCoins.forEach { coinExtractedSet.add(it.id) }
-        return ret.toMutableList()
-    }*/
-
-    override fun getOverall(): Long {
-        return coinMap.values.filterNot { coinExtractedSet.contains(it.id) }.map { it.current }.sum()
-    }
-
-    override fun getCoins(): MutableCollection<Coin>? {
-        return null
-    }
-
-    override fun listen(fn: Consumer<EmitterEvent>) {
-        listeners.add(fn)
-    }
-
-    override fun free(coin: Coin) {
-        if (coinMap.containsKey(coin.id) && coinExtractedSet.contains(coin.id)) {
-            coinFreeSet.add(coin.id)
-            log.info("emitter free ${CoinUtils.sum(listOf(coin))}, ${CoinUtils.sumString(listOf(coin))}")
-        } else {
-            throw IllegalArgumentException("coin is not free")
-        }
-    }
-
-    private suspend fun calcLater(): Set<UUID> {
-        return coinFreeSet.mapNotNull { coinMap[it] }.map { it as DemoCoin }.onEach { it._era = min(maxEraValue, it._era + 1) }.filter { it.era == maxEraValue }.map { it.id }.toSet()
-    }
-
-    override fun calc(): CompletableFuture<Any> {
-        val ret = CompletableFuture<Any>()
-        listeners.forEach { it.accept(EmitterStartRecalculationEvent()) }
-        this.deferred = CompletableDeferred()
-        deferred!!.invokeOnCompletion { t: Throwable? -> if (t == null) ret.complete(null) else ret.completeExceptionally(t) }
-        GlobalScope.launch {
-            try {
-                val nullCoins = calcLater()
-                deferred!!.complete(true)
-                listeners.forEach { it.accept(EmitterStopRecalculationEvent(nullCoins)) }
-            } catch (t: Throwable) {
-                deferred!!.completeExceptionally(t)
-            }
-        }
-        return ret
-    }
-
-    override fun emitOne(coin: Coin): UUID {
-        return computeIfAbsent(coin) { id ->
-            val new = DemoCoin(id, coin.value, coin.era)
-            log.info("emitter emit coin ${CoinUtils.sum(listOf(new))}")
-            new
-        }.id
-    }
-}
-
-val maxEraValue: Int = CoinValue.MAX_ERA.toInt()
-
-class DemoCoin(val coinId: UUID, val initialValue: CoinValue, var _era: Int = 0) : Coin {
-
-    override fun getId(): UUID = coinId
-
-    override fun getValue(): CoinValue = initialValue
-
-    override fun getEra(): Int {
-        return _era
-    }
-
-    override fun toString(): String {
-        return "${initialValue.name} jd${_era} (${current})"
-    }
-}
-
-class DemoCashbackCoin(val _value: CoinValue, val cashback: Long, val _era: Int) : Coin {
-
-    init {
-        if (cashback >= 0) throw IllegalArgumentException("cashback must be negative")
-    }
-
-    override fun getEra(): Int {
-        return _era
-    }
-
-    override fun getId(): UUID? {
-        return null
-    }
-
-    override fun getValue(): CoinValue {
-        return _value
-    }
-
-    override fun getCurrent(): Long {
-        return cashback
-    }
-
-    override fun toString(): String {
-        return "${value} jdc${era} (${current})"
-    }
-
-    /*fun invert(): DemoFutureCoin {
-        var newEra = maxEraValue - _era
-        if (newEra == maxEraValue) newEra = 0
-        var newCurrent = value.amount + cashback
-        if (newCurrent == 0L) newCurrent = value.amount
-        val ret = DemoFutureCoin(_value, newEra)
-        if (ret.current != newCurrent)
-            throw IllegalArgumentException("wrong inverted coin")
-        return ret
-    }*/
-}
-
-class DemoFutureCoin(val _value: CoinValue, val _era: Int) : Coin {
-
-    override fun getId(): UUID? {
-        return null
-    }
-
-    override fun getValue(): CoinValue {
-        return _value
-    }
-
-    override fun getEra(): Int {
-        return _era
-    }
-
-    override fun toString(): String {
-        return "${value} jdf${era} (${current})"
-    }
-}
-
 class DemoHuman(val selfId: UUID, val name: String) : NaturalPerson {
 class DemoHuman(val selfId: UUID, val name: String) : NaturalPerson {
 
 
     override fun getId(): UUID = selfId
     override fun getId(): UUID = selfId
@@ -671,236 +477,3 @@ open class DemoAccount(val accountId: UUID, personId: UUID) : BankAccount {
     }
     }
 }
 }
 
 
-
-enum class CoinStrategyType {
-    L2R_RL, L2R_R, B2R_E, L2R, E2B, L2R_E, ERR_LC, ERR_NUL;
-
-    companion object {
-        val OK = setOf(L2R_RL, L2R_R, B2R_E, L2R, E2B, L2R_E)
-        val ERR = setOf(ERR_LC, ERR_NUL)
-    }
-
-    fun isOk(): Boolean = OK.contains(this)
-
-    fun isErr(): Boolean = ERR.contains(this)
-}
-
-class CoinStrategy(val credit: Long, val leftCoins: Collection<Coin>? = null, val rightCoins: Collection<Coin>? = null) {
-    var res: CoinStrategyType? = null
-    var parent: CoinStrategy? = null
-    var leftExtract: Collection<Coin>? = null
-    var rightExtract: Collection<Coin>? = null
-    var leftCashback: Collection<Coin>? = null
-    var rightCashback: Collection<Coin>? = null
-    var leftEmit: Collection<Coin>? = null
-    var rightEmit: Collection<Coin>? = null
-}
-
-class CoinUtils {
-
-    companion object {
-
-        val avaiues = CoinValue.values().flatMap { value -> (0..maxEraValue - 1).map { era -> value to era to (value.getCurrent(era)) } }.map { it.second to it.first }.groupBy { it.first }.map { it.key to it.value.map { it.second } }.toMap()
-        val akeis = avaiues.keys.sortedDescending()
-
-        private fun makeSomeFake(cashback: Long): Collection<Coin> {
-            val ret = mutableListOf<Coin>()
-            var totalCashback = cashback
-            do {
-                val value = CoinValue.values().filter { it.amount >= Math.abs(totalCashback) }.sortedBy { it.amount }.first()
-                val valueByEra = (0..(maxEraValue - 1)).map { era -> era to (value.amount - (era * value.delta)) }.filter { it.second <= Math.abs(totalCashback) }.firstOrNull()
-                if (valueByEra != null) {
-                    ret.add(DemoCashbackCoin(value, -valueByEra.second, valueByEra.first))
-                    totalCashback += valueByEra.second
-                } else {
-                    throw RuntimeException("no value")
-                }
-            } while (totalCashback < 0)
-            return ret
-        }
-
-        private fun splitCoins(coins: Collection<Coin>, amount: Long): Collection<Coin> {
-            val ret = mutableListOf<Coin>()
-            val coinMap = coins.groupBy { it.current }.map { it.key to it.value.map { it.id }.toMutableList() }.toMap().toMutableMap()
-            val usedCoins = mutableSetOf<UUID>()
-            var totalAmount = 0L
-            do {
-                val diffAmount = amount - totalAmount
-                val leastCoinMap = coinMap.map { it.key to it.value.subtract(usedCoins).toList() }.filter { it.second.isNotEmpty() }.toMap()
-                val loeCoinMap = leastCoinMap.filter { it.key <= diffAmount }.map { it.key to it.value }.sortedByDescending { it.first }
-                if (loeCoinMap.isNotEmpty()) {
-                    val biggestLoe = loeCoinMap.first().first
-                    totalAmount += biggestLoe
-                    val coinId = loeCoinMap.first().second.first()
-                    usedCoins.add(coinId)
-                } else {
-                    val smallestGt = leastCoinMap.keys.sorted().firstOrNull()
-                    if (smallestGt != null) {
-                        val cashbackAmount = if (smallestGt >= amount && smallestGt > totalAmount) {
-                            usedCoins.clear() //одной монетой перекрываем всё предыдущее
-                            totalAmount = smallestGt
-                            val coinId = leastCoinMap.get(smallestGt)?.first()!!
-                            usedCoins.add(coinId)
-                            amount - smallestGt
-                        } else {
-                            totalAmount += smallestGt
-                            val coinId = leastCoinMap.get(smallestGt)?.first()!!
-                            usedCoins.add(coinId)
-                            diffAmount - smallestGt
-                        }
-                        ret.addAll(makeSomeFake(cashbackAmount))
-                    } else {
-                        TODO()
-                    }
-                }
-            } while (totalAmount < amount)
-            ret.addAll(coins.filter { usedCoins.contains(it.id) })
-            val realAmount = ret.filter { it.id != null }.map { it.current }.sum()
-            if (totalAmount != realAmount) {
-                TODO()
-            }
-            val fakeAmount = ret.map { it.current }.sum()
-            if (fakeAmount != amount) {
-                TODO()
-            }
-            return ret
-        }
-
-        fun sum(coins: Iterable<Coin>): Long = coins.map { it.current }.sum()
-
-        fun sumString(coins: Iterable<Coin>): String {
-            val pos = sum(coins.filter { it.id != null })
-            val neg = sum(coins.filter { it.id == null })
-            return if (neg != 0L) "${pos + neg}(${pos}${neg})" else "$pos"
-        }
-
-        fun makeSomeFuture(x: Long): List<Coin> {
-            return makeSomeVariants(x).first().flatMap { n -> (0 until n.value).map { DemoFutureCoin(n.key.first, n.key.second) } }
-        }
-
-        fun makeSomeStrategy(st: CoinStrategy): CoinStrategy {
-            return if (st.credit > 0) {
-                if (st.leftCoins != null) {
-                    if (st.credit <= sum(st.leftCoins)) {
-                        val creditCoins = splitCoins(st.leftCoins, st.credit)
-                        val creditCoinsReal = creditCoins.filter { it.id != null }
-                        val creditCoinsCashback = creditCoins.filter { it.id == null }
-                        if (creditCoinsCashback.isNotEmpty()) {
-                            val creditCashback = sum(creditCoinsReal) - st.credit
-                            if (st.rightCoins != null && creditCashback <= sum(st.rightCoins)) {
-                                if (st.leftCoins.map { it.id }.toSet().intersect(st.rightCoins.map { it.id }.toSet()).isNotEmpty()) {
-                                    throw RuntimeException("same coins, alarma")
-                                }
-                                val debitCoins = splitCoins(st.rightCoins, creditCashback)
-                                val debitCoinsReal = debitCoins.filter { it.id != null }
-                                val debitCoinsCashback = debitCoins.filter { it.id == null }
-                                if (debitCoinsCashback.isNotEmpty()) {
-                                    val debitCashback = sum(debitCoinsReal) - creditCashback
-                                    val replaceStrategy: CoinStrategy? = if (st.parent == null) {
-                                        val debitNoCashbackStrategy = CoinStrategy(st.credit + debitCashback, st.leftCoins, st.rightCoins)
-                                        debitNoCashbackStrategy.parent = st
-                                        makeSomeStrategy(debitNoCashbackStrategy)
-                                        if (setOf(CoinStrategyType.L2R_R, CoinStrategyType.L2R).contains(debitNoCashbackStrategy.res)) {
-                                            null //TODO exchange with extra, L3R_R
-                                        } else {
-                                            null
-                                        }
-                                    } else {
-                                        null
-                                    }
-                                    if (replaceStrategy != null) {
-                                        replaceStrategy
-                                    } else {
-                                        st.leftExtract = creditCoinsReal
-                                        st.leftEmit = makeSomeFuture(creditCashback)
-                                        st.rightExtract = debitCoinsReal
-                                        st.rightEmit = makeSomeFuture(debitCashback)
-                                        st.rightCashback = st.leftExtract
-                                        st.res = CoinStrategyType.L2R_RL
-                                        st
-                                    }
-                                } else {
-                                    st.leftExtract = creditCoinsReal
-                                    st.rightExtract = debitCoinsReal
-                                    st.leftCashback = st.rightExtract
-                                    st.rightCashback = st.leftExtract
-                                    st.res = CoinStrategyType.L2R_R
-                                    st
-                                }
-                            } else if (st.rightCoins == null) {
-                                st.leftExtract = creditCoinsReal
-                                st.leftEmit = makeSomeFuture(creditCashback)
-                                st.rightEmit = makeSomeFuture(st.credit)
-                                st.res = CoinStrategyType.B2R_E
-                                st
-                            } else {
-                                st.leftExtract = creditCoinsReal
-                                st.leftEmit = makeSomeFuture(creditCashback)
-                                st.rightEmit = makeSomeFuture(st.credit)
-                                st.res = CoinStrategyType.L2R_E
-                                st
-                            }
-                        } else {
-                            st.leftExtract = creditCoins
-                            st.rightCashback = creditCoins
-                            st.res = CoinStrategyType.L2R
-                            st
-                        }
-                    } else {
-                        st.res = CoinStrategyType.ERR_LC
-                        st
-                    }
-                } else {
-                    st.rightEmit = makeSomeFuture(st.credit)
-                    st.res = CoinStrategyType.E2B
-                    st
-                }
-            } else {
-                st.res = CoinStrategyType.ERR_NUL
-                st
-            }
-        }
-
-        fun makeSomeVariants(x: Long, skip: Set<Long>? = null): List<Map<Pair<CoinValue, Int>, Long>> {
-            val thisAkeis = if (skip != null && skip.isNotEmpty()) {
-                akeis.filterNot { skip.contains(it) }
-            } else {
-                akeis
-            }
-            if (!thisAkeis.contains(1)) throw IllegalArgumentException("cannot split without 1")
-            val rkeys = mutableListOf<Pair<Long, Long>>()
-            var z = x
-            do {
-                val next = thisAkeis.first { z >= it }
-                val n = z / next
-                rkeys.add(next to n)
-                z %= (n * next)
-            } while (z != 0L)
-
-            val ret = mutableListOf<Map<Pair<CoinValue, Int>, Long>>()
-            val rmap = rkeys.map { it.first to avaiues.get(it.first)?.toSet()!! }
-
-            var calcFn: ((Int, List<Pair<CoinValue, Int>>) -> Unit)? = null
-
-            calcFn = { depth, keys ->
-                if (depth < rmap.size) {
-                    rmap[depth].second.forEach { k ->
-                        calcFn!!(depth + 1, keys + listOf(k))
-                    }
-                } else if (depth == rmap.size) {
-                    calcFn!!(depth + 1, keys)
-                } else {
-                    ret.add(keys.mapIndexed { i, k -> k to rkeys[i].second }.toMap())
-                }
-            }
-
-            calcFn(0, listOf())
-
-            return if (x > 0L) {
-                ret
-            } else {
-                listOf()
-            }
-        }
-    }
-}

+ 47 - 0
src/main/kotlin/inn/ocsf/bee/freigeld/core/model/CoinDescrInv.kt

@@ -0,0 +1,47 @@
+package inn.ocsf.bee.freigeld.core.model
+
+import java.util.*
+import kotlin.math.max
+
+class CoinDescrInv(val _value: CoinValue, var cashback: Long, var _era: Int) : Coin {
+
+    init {
+        if (cashback >= 0) throw IllegalArgumentException("cashback must be negative")
+    }
+
+    override fun getEra(): Int {
+        return _era
+    }
+
+    override fun incEra() {
+        this._era = max(0, this._era - 1)
+        this.cashback = max(0, cashback + _value.delta)
+    }
+
+    override fun getId(): UUID? {
+        return null
+    }
+
+    override fun getValue(): CoinValue {
+        return _value
+    }
+
+    override fun getCurrent(): Long {
+        return cashback
+    }
+
+    override fun toString(): String {
+        return "${value} jdc${era} (${current})"
+    }
+
+    /*fun invert(): DemoFutureCoin {
+        var newEra = maxEraValue - _era
+        if (newEra == maxEraValue) newEra = 0
+        var newCurrent = value.amount + cashback
+        if (newCurrent == 0L) newCurrent = value.amount
+        val ret = DemoFutureCoin(_value, newEra)
+        if (ret.current != newCurrent)
+            throw IllegalArgumentException("wrong inverted coin")
+        return ret
+    }*/
+}

+ 1 - 2
src/main/kotlin/inn/ocsf/bee/freigeld/core/model/GlobalEmitter.kt

@@ -1,4 +1,3 @@
-import inn.ocsf.bee.freigeld.core.model.BankAccount
-import inn.ocsf.bee.freigeld.core.model.Emitter
+package inn.ocsf.bee.freigeld.core.model
 
 
 interface GlobalEmitter : Emitter, BankAccount
 interface GlobalEmitter : Emitter, BankAccount

+ 28 - 0
src/main/kotlin/inn/ocsf/bee/freigeld/core/repo/CoinRepositoryImpl.kt

@@ -0,0 +1,28 @@
+package inn.ocsf.bee.freigeld.core.repo
+
+import inn.ocsf.bee.freigeld.core.model.CoinValue
+import inn.ocsf.bee.freigeld.core.model.data.CoinData
+import inn.ocsf.bee.freigeld.core.model.data.CoinStatus
+import org.springframework.data.mongodb.core.MongoOperations
+import org.springframework.data.mongodb.core.query.Criteria
+import org.springframework.data.mongodb.core.query.Query
+import javax.inject.Inject
+
+class CoinRepositoryImpl : CoinRepositoryCustom {
+
+    @Inject
+    private lateinit var mongo: MongoOperations
+
+    override fun create(value: CoinValue, era: Int): CoinData {
+        val ret = CoinData()
+        ret.coin = CoinData.CoinImpl(value, era)
+        ret.status = CoinStatus.indoor
+        return ret
+    }
+
+    override fun getOverall(): Long {
+        return (mongo.findOne(Query.query(Criteria.where("_id").`is`("overall")), HashMap::class.java, "coin-overall-view")?.get("amount") as Long?) ?: 0L
+    }
+
+
+}

+ 21 - 1
src/test/kotlin/inn/ocsf/bee/freigeld/core/demo/CoinUtilsTest.kt

@@ -1,5 +1,6 @@
 package inn.ocsf.bee.freigeld.core.demo
 package inn.ocsf.bee.freigeld.core.demo
 
 
+import inn.ocsf.bee.freigeld.core.calc.CoinUtils
 import inn.ocsf.bee.freigeld.core.model.Coin
 import inn.ocsf.bee.freigeld.core.model.Coin
 import inn.ocsf.bee.freigeld.core.model.CoinValue
 import inn.ocsf.bee.freigeld.core.model.CoinValue
 import org.junit.jupiter.api.RepeatedTest
 import org.junit.jupiter.api.RepeatedTest
@@ -9,6 +10,25 @@ import java.util.*
 import kotlin.test.assertEquals
 import kotlin.test.assertEquals
 import kotlin.test.assertFalse
 import kotlin.test.assertFalse
 
 
+private class TestCoin(val coinId: UUID, val initialValue: CoinValue, var _era: Int = 0) : Coin {
+
+    override fun getId(): UUID = coinId
+
+    override fun getValue(): CoinValue = initialValue
+
+    override fun getEra(): Int {
+        return _era
+    }
+
+    override fun incEra() {
+        TODO("Not yet implemented")
+    }
+
+    override fun toString(): String {
+        return "${initialValue.name} jd${_era} (${current})"
+    }
+}
+
 class CoinUtilsTest {
 class CoinUtilsTest {
 
 
     val fromId = UUID.randomUUID()
     val fromId = UUID.randomUUID()
@@ -22,7 +42,7 @@ class CoinUtilsTest {
     private val log = LoggerFactory.getLogger(javaClass)
     private val log = LoggerFactory.getLogger(javaClass)
 
 
     init {
     init {
-        fromAccount.accept(mutableListOf(DemoCoin(UUID.randomUUID(), CoinValue.one)) as Collection<Coin>)
+        fromAccount.accept(mutableListOf(TestCoin(UUID.randomUUID(), CoinValue.one)) as Collection<Coin>)
     }
     }
 
 
     @Test
     @Test