From c47c7edf32d18c493552d3a4ec5bcdc82e915e57 Mon Sep 17 00:00:00 2001 From: Vihang Patil Date: Tue, 18 Dec 2018 13:55:04 +0100 Subject: [PATCH 1/2] OCS Kotlin coroutines --- .../ostelco/prime/storage/graph/Neo4jStore.kt | 204 ++++++++++--- .../prime/storage/graph/GraphStoreTest.kt | 131 +++++++- ...g.ostelco.prime.analytics.AnalyticsService | 1 + .../org.ostelco.prime.ocs.OcsAdminService | 1 - ...co.prime.paymentprocessor.PaymentProcessor | 1 + ocs-ktc/build.gradle | 16 + .../kotlin/org/ostelco/prime/ocs/OcsModule.kt | 34 +++ .../prime/ocs/analytics/AnalyticsReporter.kt | 29 ++ .../prime/ocs/consumption/Interfaces.kt | 25 ++ .../prime/ocs/consumption/OcsGrpcServer.kt | 43 +++ .../prime/ocs/consumption/OcsGrpcService.kt | 112 +++++++ .../ostelco/prime/ocs/core/OnlineCharging.kt | 110 +++++++ .../prime/ocs/notifications/Notifications.kt | 17 ++ .../io.dropwizard.jackson.Discoverable | 1 + .../org.ostelco.prime.module.PrimeModule | 1 + prime-modules/build.gradle | 1 + .../ostelco/prime/ocs/OcsSubscriberService.kt | 7 + .../org/ostelco/prime/storage/Variants.kt | 21 +- prime/build.gradle | 2 +- prime/infra/dev/prime.yaml | 8 +- prime/infra/prod/prime.yaml | 8 +- .../kotlin/org/ostelco/prime/ocs/OcsTest.kt | 279 ------------------ .../prime/storage/graph/Neo4jStorageTest.kt | 5 - settings.gradle | 2 + 24 files changed, 727 insertions(+), 332 deletions(-) create mode 100644 neo4j-store/src/test/resources/META-INF/services/org.ostelco.prime.analytics.AnalyticsService delete mode 100644 neo4j-store/src/test/resources/META-INF/services/org.ostelco.prime.ocs.OcsAdminService create mode 100644 neo4j-store/src/test/resources/META-INF/services/org.ostelco.prime.paymentprocessor.PaymentProcessor create mode 100644 ocs-ktc/build.gradle create mode 100644 ocs-ktc/src/main/kotlin/org/ostelco/prime/ocs/OcsModule.kt create mode 100644 ocs-ktc/src/main/kotlin/org/ostelco/prime/ocs/analytics/AnalyticsReporter.kt create mode 100644 ocs-ktc/src/main/kotlin/org/ostelco/prime/ocs/consumption/Interfaces.kt create mode 100644 ocs-ktc/src/main/kotlin/org/ostelco/prime/ocs/consumption/OcsGrpcServer.kt create mode 100644 ocs-ktc/src/main/kotlin/org/ostelco/prime/ocs/consumption/OcsGrpcService.kt create mode 100644 ocs-ktc/src/main/kotlin/org/ostelco/prime/ocs/core/OnlineCharging.kt create mode 100644 ocs-ktc/src/main/kotlin/org/ostelco/prime/ocs/notifications/Notifications.kt create mode 100644 ocs-ktc/src/main/resources/META-INF/services/io.dropwizard.jackson.Discoverable create mode 100644 ocs-ktc/src/main/resources/META-INF/services/org.ostelco.prime.module.PrimeModule delete mode 100644 prime/src/integration-tests/kotlin/org/ostelco/prime/ocs/OcsTest.kt diff --git a/neo4j-store/src/main/kotlin/org/ostelco/prime/storage/graph/Neo4jStore.kt b/neo4j-store/src/main/kotlin/org/ostelco/prime/storage/graph/Neo4jStore.kt index ecda02612..0f98b292c 100644 --- a/neo4j-store/src/main/kotlin/org/ostelco/prime/storage/graph/Neo4jStore.kt +++ b/neo4j-store/src/main/kotlin/org/ostelco/prime/storage/graph/Neo4jStore.kt @@ -3,23 +3,55 @@ package org.ostelco.prime.storage.graph import arrow.core.Either import arrow.core.fix import arrow.core.flatMap +import arrow.core.left +import arrow.core.right import arrow.effects.IO import arrow.instances.either.monad.monad import arrow.typeclasses.binding import org.neo4j.driver.v1.Transaction import org.ostelco.prime.analytics.AnalyticsService import org.ostelco.prime.getLogger -import org.ostelco.prime.model.* +import org.ostelco.prime.model.Bundle +import org.ostelco.prime.model.ChangeSegment +import org.ostelco.prime.model.Offer +import org.ostelco.prime.model.Plan +import org.ostelco.prime.model.Product +import org.ostelco.prime.model.ProductClass +import org.ostelco.prime.model.PurchaseRecord +import org.ostelco.prime.model.RefundRecord +import org.ostelco.prime.model.ScanInformation +import org.ostelco.prime.model.ScanStatus +import org.ostelco.prime.model.Segment +import org.ostelco.prime.model.Subscriber +import org.ostelco.prime.model.SubscriberState +import org.ostelco.prime.model.SubscriberStatus +import org.ostelco.prime.model.Subscription import org.ostelco.prime.module.getResource import org.ostelco.prime.notifications.NOTIFY_OPS_MARKER -import org.ostelco.prime.ocs.OcsAdminService -import org.ostelco.prime.ocs.OcsSubscriberService import org.ostelco.prime.paymentprocessor.PaymentProcessor -import org.ostelco.prime.paymentprocessor.core.* -import org.ostelco.prime.storage.* +import org.ostelco.prime.paymentprocessor.core.BadGatewayError +import org.ostelco.prime.paymentprocessor.core.ForbiddenError +import org.ostelco.prime.paymentprocessor.core.PaymentError +import org.ostelco.prime.paymentprocessor.core.ProductInfo +import org.ostelco.prime.paymentprocessor.core.ProfileInfo +import org.ostelco.prime.storage.AlreadyExistsError +import org.ostelco.prime.storage.GraphStore +import org.ostelco.prime.storage.NotCreatedError +import org.ostelco.prime.storage.NotDeletedError import org.ostelco.prime.storage.NotFoundError +import org.ostelco.prime.storage.NotUpdatedError +import org.ostelco.prime.storage.StoreError +import org.ostelco.prime.storage.ValidationError import org.ostelco.prime.storage.graph.Graph.read -import org.ostelco.prime.storage.graph.Relation.* +import org.ostelco.prime.storage.graph.Graph.write +import org.ostelco.prime.storage.graph.Relation.BELONG_TO_SEGMENT +import org.ostelco.prime.storage.graph.Relation.HAS_BUNDLE +import org.ostelco.prime.storage.graph.Relation.HAS_SUBSCRIPTION +import org.ostelco.prime.storage.graph.Relation.LINKED_TO_BUNDLE +import org.ostelco.prime.storage.graph.Relation.OFFERED_TO_SEGMENT +import org.ostelco.prime.storage.graph.Relation.OFFER_HAS_PRODUCT +import org.ostelco.prime.storage.graph.Relation.PURCHASED +import org.ostelco.prime.storage.graph.Relation.REFERRED import java.time.Instant import java.util.* import java.util.stream.Collectors @@ -44,7 +76,6 @@ class Neo4jStore : GraphStore by Neo4jStoreSingleton object Neo4jStoreSingleton : GraphStore { - private val ocsAdminService: OcsAdminService by lazy { getResource() } private val logger by getLogger() // @@ -208,7 +239,6 @@ object Neo4jStoreSingleton : GraphStore { subscriber.id, PurchaseRecord(id = UUID.randomUUID().toString(), product = product, timestamp = Instant.now().toEpochMilli(), msisdn = ""), transaction) - ocsAdminService.addBundle(Bundle(bundleId, balance)) subscriberToBundleStore.create(subscriber.id, bundleId, transaction).bind() // TODO Remove hardcoded country code. // https://docs.oracle.com/javase/9/docs/api/java/util/Locale.IsoCountryCode.html @@ -260,7 +290,6 @@ object Neo4jStoreSingleton : GraphStore { val subscriber = subscriberStore.get(subscriberId, transaction).bind() bundles.forEach { bundle -> subscriptionToBundleStore.create(subscription, bundle, transaction).bind() - ocsAdminService.addMsisdnToBundleMapping(msisdn, bundle.id) } subscriptionRelationStore.create(subscriber, subscription, transaction).bind() // TODO Remove hardcoded country code. @@ -344,13 +373,87 @@ object Neo4jStoreSingleton : GraphStore { } // - // Purchase Records + // Consumption + // + + /** + * This method takes [msisdn], [usedBytes] and [requestedBytes] as parameters. + * The [usedBytes] will then be deducted from existing `balance` and `reservedBytes` from a [Bundle] associated with + * this [msisdn]. + * Thus, `reservedBytes` is set back to `zero` and `surplus/deficit` bytes are adjusted with main `balance`. + * Then, bytes equal to or less than [requestedBytes] are deducted from `balance` such that `balance` is `non-negative`. + * Those bytes are then set as `reservedBytes` and returned as response. + * + * The above logic is vanilla case and may be enriched based on multiple factors such as mcc_mnc, rating-group etc. + * + * @param msisdn which is consuming + * @param usedBytes Bytes already consumed. + * @param requestedBytes Bytes requested for consumption. + * + */ + override fun consume(msisdn: String, usedBytes: Long, requestedBytes: Long): Either> { + + // Note: _LOCK_ dummy property is set in the relation 'r' and node 'bundle' so that they get locked. + // Ref: https://neo4j.com/docs/java-reference/current/transactions/#transactions-isolation + + return writeTransaction { + IO { + Either.monad().binding { + val (reservedBytes, balance) = read(""" + MATCH (:${subscriptionEntity.name} {id: '$msisdn'})-[r:${subscriptionToBundleRelation.relation.name}]->(bundle:${bundleEntity.name}) + SET r._LOCK_ = true, bundle._LOCK_ = true + RETURN r.reservedBytes AS reservedBytes, bundle.balance AS balance + """.trimIndent(), + transaction) { statementResult -> + if (statementResult.hasNext()) { + val record = statementResult.single() + val reservedBytes = record.get("reservedBytes").asString("0").toLong() + val balance = record.get("balance").asString("0").toLong() + Pair(reservedBytes, balance).right() + } else { + NotFoundError("Bundle for ${subscriptionEntity.name}", msisdn).left() + } + }.bind() + + // First adjust reserved and used bytes + // Balance cannot be negative + var newBalance = Math.max(balance + reservedBytes - usedBytes, 0) + + // Then check how much of requested bytes can be granted + val granted = Math.min(newBalance, requestedBytes) + newBalance -= granted + + write(""" + MATCH (:${subscriptionEntity.name} {id: '$msisdn'})-[r:${subscriptionToBundleRelation.relation.name}]->(bundle:${bundleEntity.name}) + SET r.reservedBytes = '$granted', bundle.balance = '$newBalance' + REMOVE r._LOCK_, bundle._LOCK_ + RETURN r.reservedBytes AS granted, bundle.balance AS balance + """.trimIndent(), + transaction) { statementResult -> + if (statementResult.hasNext()) { + val record = statementResult.single() + val savedBalance = record.get("balance").asString("0").toLong() + if (savedBalance != newBalance) { + logger.error(NOTIFY_OPS_MARKER, "Wrong balance set for msisdn: $msisdn to instead of $newBalance") + } + val savedGranted = record.get("granted").asString("0").toLong() + Pair(savedGranted, savedBalance).right() + } else { + NotUpdatedError("Balance for ${subscriptionEntity.name}", msisdn).left() + } + }.bind() + }.fix() + }.unsafeRunSync()//.ifFailedThenRollback(transaction) + } + } + + // + // Purchase // // TODO vihang: Move this logic to DSL + Rule Engine + Triggers, when they are ready // >> BEGIN private val paymentProcessor by lazy { getResource() } - private val ocs by lazy { getResource() } private val analyticsReporter by lazy { getResource() } private fun fetchOrCreatePaymentProfile(subscriberId: String): Either = @@ -375,10 +478,22 @@ object Neo4jStoreSingleton : GraphStore { { /* TODO: Complete support for 'product-class' and store plans as a 'product' of product-class: 'plan'. */ - return if (it.properties.containsKey("productType") && it.properties["productType"].equals("plan", true)) - purchasePlan(subscriberId, it, sourceId, saveCard) - else - purchaseProduct(subscriberId, it, sourceId, saveCard) + return if (it.properties.containsKey("productType") + && it.properties["productType"].equals("plan", true)) { + + purchasePlan( + subscriberId = subscriberId, + product = it, + sourceId = sourceId, + saveCard = saveCard) + } else { + + purchaseProduct( + subscriberId = subscriberId, + product = it, + sourceId = sourceId, + saveCard = saveCard) + } } ) } @@ -471,10 +586,29 @@ object Neo4jStoreSingleton : GraphStore { //TODO: While aborting transactions, send a record with "reverted" status analyticsReporter.reportPurchaseInfo(purchaseRecord, subscriberId, "success") - ocs.topup(subscriberId, product.sku) - .mapLeft { - BadGatewayError("Failed to perform topup", message = it) - }.bind() + + // Topup + val bytes = product.properties["noOfBytes"]?.replace("_","")?.toLongOrNull() ?: 0L + + if (bytes == 0L) { + logger.error("Product with 0 bytes: sku = ${product.sku}") + } + + write(""" + MATCH (sr:${subscriberEntity.name} { id:'$subscriberId' })-[:${subscriberToBundleRelation.relation.name}]->(bundle:${bundleEntity.name}) + SET bundle.balance = toString(toInteger(bundle.balance) + $bytes) + """.trimIndent(), transaction) { + Either.cond( + test = it.summary().counters().containsUpdates(), + ifTrue = {}, + ifFalse = { + logger.error("Failed to update balance during purchase for subscriber: $subscriberId") + BadGatewayError( + description = "Failed to update balance during purchase for subscriber: $subscriberId", + message = "Failed to perform topup") + }) + }.bind() + // Even if the "capture charge operation" failed, we do not want to rollback. // In that case, we just want to log it at error level. // These transactions can then me manually changed before they are auto rollback'ed in 'X' days. @@ -486,10 +620,8 @@ object Neo4jStoreSingleton : GraphStore { // Ignore failure to capture charge, by not calling bind() ProductInfo(product.sku) }.fix() - }.unsafeRunSync() - .ifFailedThenRollback(transaction) + }.unsafeRunSync().ifFailedThenRollback(transaction) } - // << END private fun removePaymentSource(saveCard: Boolean, paymentCustomerId: String, sourceId: String) { @@ -504,6 +636,10 @@ object Neo4jStoreSingleton : GraphStore { } } + // + // Purchase Records + // + override fun getPurchaseRecords(subscriberId: String): Either> { return readTransaction { subscriberStore.getRelations(subscriberId, purchaseRecordRelation, transaction) @@ -590,7 +726,7 @@ object Neo4jStoreSingleton : GraphStore { val scanId = UUID.randomUUID().toString() val newScan = ScanInformation(scanId = scanId, status = ScanStatus.PENDING, scanResult = null) scanInformationStore.create(newScan, transaction).flatMap { - scanInformationRelationStore.create(subscriber.id, newScan.id, transaction). flatMap { Either.right(newScan) } + scanInformationRelationStore.create(subscriber.id, newScan.id, transaction).flatMap { Either.right(newScan) } } } } @@ -600,7 +736,7 @@ object Neo4jStoreSingleton : GraphStore { MATCH (subscriber:${subscriberEntity.name})-[:${scanInformationRelation.relation.name}]->(scanInformation:${scanInformationEntity.name} {scanId: '${scanId}'}) RETURN subscriber """.trimIndent(), - transaction) { + transaction) { if (it.hasNext()) Either.right(subscriberEntity.createEntity(it.single().get("subscriber").asMap())) else @@ -620,6 +756,7 @@ object Neo4jStoreSingleton : GraphStore { } } } + override fun getAllScanInformation(subscriberId: String): Either> = readTransaction { subscriberStore.getRelated(subscriberId, scanInformationRelation, transaction) } @@ -629,11 +766,11 @@ object Neo4jStoreSingleton : GraphStore { getSubscriberId(scanInformation.scanId, transaction).flatMap { subscriber -> scanInformationStore.update(scanInformation, transaction).flatMap { logger.info("updating scan Information for : ${subscriber.email} id: ${scanInformation.scanId} status: ${scanInformation.status}") - getOrCreateSubscriberState(subscriber.id, SubscriberStatus.REGISTERED, transaction).flatMap {subcriberState -> - if (scanInformation.status == ScanStatus.APPROVED && (subcriberState.status == SubscriberStatus.REGISTERED || subcriberState.status == SubscriberStatus.EKYC_REJECTED)) { + getOrCreateSubscriberState(subscriber.id, SubscriberStatus.REGISTERED, transaction).flatMap { subcriberState -> + if (scanInformation.status == ScanStatus.APPROVED && (subcriberState.status == SubscriberStatus.REGISTERED || subcriberState.status == SubscriberStatus.EKYC_REJECTED)) { // Update the state if the scan was successul and we are waiting for eKYC results updateSubscriberState(subscriber.id, SubscriberStatus.EKYC_APPROVED, transaction).map { Unit } - } else if (scanInformation.status == ScanStatus.REJECTED && subcriberState.status == SubscriberStatus.REGISTERED) { + } else if (scanInformation.status == ScanStatus.REJECTED && subcriberState.status == SubscriberStatus.REGISTERED) { // Update the state if the scan was a failure and we are waiting for eKYC results updateSubscriberState(subscriber.id, SubscriberStatus.EKYC_REJECTED, transaction).map { Unit } } else { @@ -720,6 +857,7 @@ object Neo4jStoreSingleton : GraphStore { // // For metrics // + override fun getSubscriberCount(): Long = readTransaction { read(""" MATCH (subscriber:${subscriberEntity.name}) @@ -817,8 +955,8 @@ object Neo4jStoreSingleton : GraphStore { productStore.create(product, transaction) .bind() plansStore.create(plan.copy(properties = plan.properties.plus(mapOf( - "planId" to planInfo.id, - "productId" to productInfo.id))), transaction) + "planId" to planInfo.id, + "productId" to productInfo.id))), transaction) .bind() planProductRelationStore.create(plan.id, product.id, transaction) .bind() @@ -889,7 +1027,7 @@ object Neo4jStoreSingleton : GraphStore { /* Lookup in payment backend will fail if no value found for 'planId'. */ val subscriptionInfo = paymentProcessor.createSubscription(plan.properties.getOrDefault("planId", "missing"), - profileInfo.id, trialEnd) + profileInfo.id, trialEnd) .mapLeft { NotCreatedError(type = planEntity.name, id = "Failed to subscribe ${subscriberId} to ${plan.id}", error = it) @@ -899,8 +1037,8 @@ object Neo4jStoreSingleton : GraphStore { /* Store information from payment backend for later use. */ subscribesToPlanRelationStore.setProperties(subscriberId, planId, mapOf("subscriptionId" to subscriptionInfo.id, - "created" to subscriptionInfo.created, - "trialEnd" to subscriptionInfo.trialEnd), transaction) + "created" to subscriptionInfo.created, + "trialEnd" to subscriptionInfo.trialEnd), transaction) .flatMap { Either.right(plan) }.bind() @@ -954,7 +1092,7 @@ object Neo4jStoreSingleton : GraphStore { }.bind() }.fix() }.unsafeRunSync() - .ifFailedThenRollback(transaction) + .ifFailedThenRollback(transaction) } // diff --git a/neo4j-store/src/test/kotlin/org/ostelco/prime/storage/graph/GraphStoreTest.kt b/neo4j-store/src/test/kotlin/org/ostelco/prime/storage/graph/GraphStoreTest.kt index 2488688d3..2ff4d7109 100644 --- a/neo4j-store/src/test/kotlin/org/ostelco/prime/storage/graph/GraphStoreTest.kt +++ b/neo4j-store/src/test/kotlin/org/ostelco/prime/storage/graph/GraphStoreTest.kt @@ -1,5 +1,6 @@ package org.ostelco.prime.storage.graph +import arrow.core.right import com.palantir.docker.compose.DockerComposeRule import com.palantir.docker.compose.connection.waiting.HealthChecks import org.joda.time.Duration @@ -8,8 +9,19 @@ import org.junit.BeforeClass import org.junit.ClassRule import org.mockito.Mockito import org.neo4j.driver.v1.AccessMode.WRITE -import org.ostelco.prime.model.* -import org.ostelco.prime.ocs.OcsAdminService +import org.ostelco.prime.analytics.AnalyticsService +import org.ostelco.prime.model.Offer +import org.ostelco.prime.model.Price +import org.ostelco.prime.model.Product +import org.ostelco.prime.model.PurchaseRecord +import org.ostelco.prime.model.ScanInformation +import org.ostelco.prime.model.ScanResult +import org.ostelco.prime.model.ScanStatus +import org.ostelco.prime.model.Segment +import org.ostelco.prime.model.Subscriber +import org.ostelco.prime.model.Subscription +import org.ostelco.prime.paymentprocessor.PaymentProcessor +import org.ostelco.prime.paymentprocessor.core.ProfileInfo import java.time.Instant import java.util.* import kotlin.test.BeforeTest @@ -19,7 +31,11 @@ import kotlin.test.assertNotNull import kotlin.test.assertTrue import kotlin.test.fail -class MockOcsAdminService : OcsAdminService by Mockito.mock(OcsAdminService::class.java) +private val mockPaymentProcessor = Mockito.mock(PaymentProcessor::class.java) +class MockPaymentProcessor : PaymentProcessor by mockPaymentProcessor + +private val mockAnalyticsService = Mockito.mock(AnalyticsService::class.java) +class MockAnalyticsService : AnalyticsService by mockAnalyticsService class GraphStoreTest { @@ -47,7 +63,7 @@ class GraphStoreTest { } @Test - fun `add subscriber`() { + fun `test - add subscriber`() { Neo4jStoreSingleton.addSubscriber(Subscriber(email = EMAIL, name = NAME, country = COUNTRY), referredBy = null) .mapLeft { fail(it.message) } @@ -63,7 +79,7 @@ class GraphStoreTest { } @Test - fun `fail to add subscriber with invalid referred by`() { + fun `test - fail to add subscriber with invalid referred by`() { Neo4jStoreSingleton.addSubscriber(Subscriber(email = EMAIL, name = NAME, country = COUNTRY), referredBy = "blah") .fold({ @@ -75,7 +91,7 @@ class GraphStoreTest { } @Test - fun `add subscription`() { + fun `test - add subscription`() { Neo4jStoreSingleton.addSubscriber(Subscriber(email = EMAIL, name = NAME, country = COUNTRY), referredBy = null) .mapLeft { fail(it.message) } @@ -99,6 +115,103 @@ class GraphStoreTest { // assertEquals(EMAIL, bundleIdArgCaptor.value) } + @Test + fun `test - purchase`() { + + val sku = "1GB_249NOK" + val chargeId = UUID.randomUUID().toString() + // mock + Mockito.`when`(mockPaymentProcessor.getPaymentProfile(userEmail = EMAIL)) + .thenReturn(ProfileInfo(EMAIL).right()) + + Mockito.`when`(mockPaymentProcessor.authorizeCharge( + customerId = EMAIL, + sourceId = null, + amount = 24900, + currency = "NOK") + ).thenReturn(chargeId.right()) + + Mockito.`when`(mockPaymentProcessor.captureCharge( + customerId = EMAIL, + amount = 24900, + currency = "NOK", + chargeId = chargeId) + ).thenReturn(chargeId.right()) + + // prep + Neo4jStoreSingleton.addSubscriber(Subscriber(email = EMAIL, name = NAME, country = COUNTRY), referredBy = null) + .mapLeft { fail(it.message) } + + Neo4jStoreSingleton.addSubscription(EMAIL, MSISDN) + .mapLeft { fail(it.message) } + + Neo4jStoreSingleton.createProduct(createProduct(sku = sku, amount = 24900)) + .mapLeft { fail(it.message) } + + val offer = Offer( + id = "NEW_OFFER", + segments = listOf(getSegmentNameFromCountryCode(COUNTRY)), + products = listOf(sku)) + + Neo4jStoreSingleton.createOffer(offer) + .mapLeft { fail(it.message) } + + // test + Neo4jStoreSingleton.purchaseProduct(subscriberId = EMAIL, sku = sku, sourceId = null, saveCard = false) + .mapLeft { fail(it.message) } + + // assert + Neo4jStoreSingleton.getBundles(subscriberId = EMAIL).bimap( + { fail(it.message) }, + { bundles -> + bundles.forEach { bundle -> + assertEquals(1_100_000_000L, bundle.balance) + } + }) + } + + @Test + fun `test - consume`() { + Neo4jStoreSingleton.addSubscriber(Subscriber(email = EMAIL, name = NAME, country = COUNTRY), referredBy = null) + .mapLeft { fail(it.message) } + + Neo4jStoreSingleton.addSubscription(EMAIL, MSISDN) + .mapLeft { fail(it.message) } + + // balance = 100_000_000 + // reserved = 0 + + // requested = 40_000_000 + val dataBucketSize = 40_000_000L + Neo4jStoreSingleton.consume(msisdn = MSISDN, usedBytes = 0, requestedBytes = dataBucketSize) + .fold( + { fail(it.message) }, + { + assertEquals(dataBucketSize, it.first) // reserved = 40_000_000 + assertEquals(60_000_000L, it.second) // balance = 60_000_000 + }) + + // used = 50_000_000 + // requested = 40_000_000 + Neo4jStoreSingleton.consume(msisdn = MSISDN, usedBytes = 50_000_000L, requestedBytes = dataBucketSize) + .fold( + { fail(it.message) }, + { + assertEquals(dataBucketSize, it.first) // reserved = 40_000_000 + assertEquals(10_000_000L, it.second) // balance = 10_000_000 + }) + + // used = 30_000_000 + // requested = 40_000_000 + Neo4jStoreSingleton.consume(msisdn = MSISDN, usedBytes = 30_000_000L, requestedBytes = dataBucketSize) + .fold( + { fail(it.message) }, + { + assertEquals(20_000_000L, it.first) // reserved = 20_000_000 + assertEquals(0L, it.second) // balance = 0 + }) + } + @Test fun `set and get Purchase record`() { assert(Neo4jStoreSingleton.addSubscriber(Subscriber(email = EMAIL, name = NAME, country = COUNTRY), referredBy = null).isRight()) @@ -126,20 +239,26 @@ class GraphStoreTest { assert(Neo4jStoreSingleton.addSubscriber(Subscriber(email = EMAIL, name = NAME, country = COUNTRY), referredBy = null).isRight()) Neo4jStoreSingleton.createProduct(createProduct("1GB_249NOK", 24900)) + .mapLeft { fail(it.message) } Neo4jStoreSingleton.createProduct(createProduct("2GB_299NOK", 29900)) + .mapLeft { fail(it.message) } Neo4jStoreSingleton.createProduct(createProduct("3GB_349NOK", 34900)) + .mapLeft { fail(it.message) } Neo4jStoreSingleton.createProduct(createProduct("5GB_399NOK", 39900)) + .mapLeft { fail(it.message) } val segment = Segment( id = "NEW_SEGMENT", subscribers = listOf(EMAIL)) Neo4jStoreSingleton.createSegment(segment) + .mapLeft { fail(it.message) } val offer = Offer( id = "NEW_OFFER", segments = listOf("NEW_SEGMENT"), products = listOf("3GB_349NOK")) Neo4jStoreSingleton.createOffer(offer) + .mapLeft { fail(it.message) } Neo4jStoreSingleton.getProducts(EMAIL).bimap( { fail(it.message) }, diff --git a/neo4j-store/src/test/resources/META-INF/services/org.ostelco.prime.analytics.AnalyticsService b/neo4j-store/src/test/resources/META-INF/services/org.ostelco.prime.analytics.AnalyticsService new file mode 100644 index 000000000..1a739ef6e --- /dev/null +++ b/neo4j-store/src/test/resources/META-INF/services/org.ostelco.prime.analytics.AnalyticsService @@ -0,0 +1 @@ +org.ostelco.prime.storage.graph.MockAnalyticsService \ No newline at end of file diff --git a/neo4j-store/src/test/resources/META-INF/services/org.ostelco.prime.ocs.OcsAdminService b/neo4j-store/src/test/resources/META-INF/services/org.ostelco.prime.ocs.OcsAdminService deleted file mode 100644 index b3b33a72f..000000000 --- a/neo4j-store/src/test/resources/META-INF/services/org.ostelco.prime.ocs.OcsAdminService +++ /dev/null @@ -1 +0,0 @@ -org.ostelco.prime.storage.graph.MockOcsAdminService \ No newline at end of file diff --git a/neo4j-store/src/test/resources/META-INF/services/org.ostelco.prime.paymentprocessor.PaymentProcessor b/neo4j-store/src/test/resources/META-INF/services/org.ostelco.prime.paymentprocessor.PaymentProcessor new file mode 100644 index 000000000..c919e0135 --- /dev/null +++ b/neo4j-store/src/test/resources/META-INF/services/org.ostelco.prime.paymentprocessor.PaymentProcessor @@ -0,0 +1 @@ +org.ostelco.prime.storage.graph.MockPaymentProcessor \ No newline at end of file diff --git a/ocs-ktc/build.gradle b/ocs-ktc/build.gradle new file mode 100644 index 000000000..46aae68e6 --- /dev/null +++ b/ocs-ktc/build.gradle @@ -0,0 +1,16 @@ +plugins { + id "org.jetbrains.kotlin.jvm" version "1.3.10" + id "java-library" +} + +dependencies { + implementation project(':prime-modules') + + implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$kotlinXCoroutinesVersion" + + testImplementation "org.jetbrains.kotlin:kotlin-test:$kotlinVersion" + testImplementation "org.jetbrains.kotlin:kotlin-test-junit:$kotlinVersion" + testImplementation "org.mockito:mockito-core:$mockitoVersion" +} + +apply from: '../gradle/jacoco.gradle' \ No newline at end of file diff --git a/ocs-ktc/src/main/kotlin/org/ostelco/prime/ocs/OcsModule.kt b/ocs-ktc/src/main/kotlin/org/ostelco/prime/ocs/OcsModule.kt new file mode 100644 index 000000000..99f9dec84 --- /dev/null +++ b/ocs-ktc/src/main/kotlin/org/ostelco/prime/ocs/OcsModule.kt @@ -0,0 +1,34 @@ +package org.ostelco.prime.ocs + +import com.fasterxml.jackson.annotation.JsonProperty +import com.fasterxml.jackson.annotation.JsonTypeName +import io.dropwizard.setup.Environment +import org.hibernate.validator.constraints.NotEmpty +import org.ostelco.prime.module.PrimeModule +import org.ostelco.prime.ocs.consumption.OcsGrpcServer +import org.ostelco.prime.ocs.consumption.OcsGrpcService +import org.ostelco.prime.ocs.core.OnlineCharging + +@JsonTypeName("ocs") +class OcsModule : PrimeModule { + + @JsonProperty + fun setConfig(config: Config) { + ConfigRegistry.config = config + } + + override fun init(env: Environment) { + env.lifecycle().manage( + OcsGrpcServer(8082, OcsGrpcService(OnlineCharging))) + } +} + +class Config { + @NotEmpty + @JsonProperty("lowBalanceThreshold") + var lowBalanceThreshold: Long = 0 +} + +object ConfigRegistry { + lateinit var config: Config +} \ No newline at end of file diff --git a/ocs-ktc/src/main/kotlin/org/ostelco/prime/ocs/analytics/AnalyticsReporter.kt b/ocs-ktc/src/main/kotlin/org/ostelco/prime/ocs/analytics/AnalyticsReporter.kt new file mode 100644 index 000000000..f80ad5be3 --- /dev/null +++ b/ocs-ktc/src/main/kotlin/org/ostelco/prime/ocs/analytics/AnalyticsReporter.kt @@ -0,0 +1,29 @@ +package org.ostelco.prime.ocs.analytics + +import org.ostelco.ocs.api.CreditControlRequestInfo +import org.ostelco.prime.analytics.AnalyticsService +import org.ostelco.prime.getLogger +import org.ostelco.prime.module.getResource + +/** + * This class publishes the data consumption information events analytics. + */ +object AnalyticsReporter { + + private val logger by getLogger() + + private val analyticsReporter by lazy { getResource() } + + fun report(request: CreditControlRequestInfo, bundleBytes: Long) { + val msisdn = request.msisdn + if (msisdn != null) { + logger.info("Sent Data Consumption info event to analytics") + analyticsReporter.reportTrafficInfo( + msisdn = msisdn, + usedBytes = request.msccList?.firstOrNull()?.used?.totalOctets ?: 0L, + bundleBytes = bundleBytes, + apn = request.serviceInformation?.psInformation?.calledStationId, + mccMnc = request.serviceInformation?.psInformation?.sgsnMccMnc) + } + } +} diff --git a/ocs-ktc/src/main/kotlin/org/ostelco/prime/ocs/consumption/Interfaces.kt b/ocs-ktc/src/main/kotlin/org/ostelco/prime/ocs/consumption/Interfaces.kt new file mode 100644 index 000000000..7938bb4a2 --- /dev/null +++ b/ocs-ktc/src/main/kotlin/org/ostelco/prime/ocs/consumption/Interfaces.kt @@ -0,0 +1,25 @@ +package org.ostelco.prime.ocs.consumption + +import io.grpc.stub.StreamObserver +import org.ostelco.ocs.api.ActivateResponse +import org.ostelco.ocs.api.CreditControlAnswerInfo +import org.ostelco.ocs.api.CreditControlRequestInfo + +/** + * Ocs Requests from [OcsGrpcService] are consumed by implementation [OcsService] of [OcsAsyncRequestConsumer] + */ +interface OcsAsyncRequestConsumer { + fun putCreditControlClient(streamId: String, creditControlAnswer: StreamObserver) + fun creditControlRequestEvent(streamId: String, request: CreditControlRequestInfo) + fun deleteCreditControlClient(streamId: String) + fun updateActivateResponse(streamId: String, activateResponse: StreamObserver) +} + +/** + * Ocs Events from [OcsEventToGrpcResponseMapper] forwarded to implementation [OcsService] of [OcsAsyncResponseProducer] + */ +interface OcsAsyncResponseProducer { + fun activateOnNextResponse(response: ActivateResponse) + fun sendCreditControlAnswer(streamId: String, creditControlAnswer: CreditControlAnswerInfo) + fun returnUnusedDataBucketEvent(msisdn: String, reservedBucketBytes: Long) +} \ No newline at end of file diff --git a/ocs-ktc/src/main/kotlin/org/ostelco/prime/ocs/consumption/OcsGrpcServer.kt b/ocs-ktc/src/main/kotlin/org/ostelco/prime/ocs/consumption/OcsGrpcServer.kt new file mode 100644 index 000000000..bff7eba97 --- /dev/null +++ b/ocs-ktc/src/main/kotlin/org/ostelco/prime/ocs/consumption/OcsGrpcServer.kt @@ -0,0 +1,43 @@ +package org.ostelco.prime.ocs.consumption + +import io.dropwizard.lifecycle.Managed +import io.grpc.BindableService +import io.grpc.Server +import io.grpc.ServerBuilder +import org.ostelco.prime.getLogger + +/** + * This is OCS Server running on gRPC protocol. + * Its startup and shutdown are managed by Dropwizard's lifecycle + * through the Managed interface. + * + */ +class OcsGrpcServer(private val port: Int, service: BindableService) : Managed { + + private val logger by getLogger() + + // may add Transport Security with Certificates if needed. + // may add executor for control over number of threads + private val server: Server = ServerBuilder.forPort(port).addService(service).build() + + override fun start() { + server.start() + logger.info("OcsServer Server started, listening for incoming gRPC traffic on {}", port) + } + + override fun stop() { + logger.info("Stopping OcsServer Server listening for gRPC traffic on {}", port) + server.shutdown() + blockUntilShutdown() + } + + // Used for unit testing + fun forceStop() { + logger.info("Stopping forcefully OcsServer Server listening for gRPC traffic on {}", port) + server.shutdownNow() + } + + private fun blockUntilShutdown() { + server.awaitTermination() + } +} diff --git a/ocs-ktc/src/main/kotlin/org/ostelco/prime/ocs/consumption/OcsGrpcService.kt b/ocs-ktc/src/main/kotlin/org/ostelco/prime/ocs/consumption/OcsGrpcService.kt new file mode 100644 index 000000000..2ca3d0432 --- /dev/null +++ b/ocs-ktc/src/main/kotlin/org/ostelco/prime/ocs/consumption/OcsGrpcService.kt @@ -0,0 +1,112 @@ +package org.ostelco.prime.ocs.consumption + +import io.grpc.stub.StreamObserver +import org.ostelco.ocs.api.ActivateRequest +import org.ostelco.ocs.api.ActivateResponse +import org.ostelco.ocs.api.CreditControlAnswerInfo +import org.ostelco.ocs.api.CreditControlRequestInfo +import org.ostelco.ocs.api.CreditControlRequestType.NONE +import org.ostelco.ocs.api.OcsServiceGrpc +import org.ostelco.prime.getLogger +import java.util.* + +/** + * A service that can be used to serve incoming GRPC requests. + * is typically bound to a service port using the GRPC ServerBuilder mechanism + * provide by GRPC: + * + * ` + * server = ServerBuilder. + * forPort(port). + * addService(service). + * build(); +` * + * + * It's implemented as a subclass of [OcsServiceGrpc.OcsServiceImplBase] overriding + * methods that together implements the protocol described in the ocs.proto file: + * + * ` + * // OCS Service + * service OcsService { + * rpc CreditControlRequest (stream CreditControlRequestInfo) returns (stream CreditControlAnswerInfo) {} + * rpc Activate (ActivateRequest) returns (stream ActivateResponse) {} + * } +` * + * + * The "stream" type parameters represents sequences of responses, so typically we will here + * see that a client invokes a method, and listens for a stream of information related to + * that particular stream. + */ +class OcsGrpcService(private val ocsAsyncRequestConsumer: OcsAsyncRequestConsumer) : OcsServiceGrpc.OcsServiceImplBase() { + + private val logger by getLogger() + + /** + * Method to handle Credit-Control-Requests + * + * @param creditControlAnswer Stream used to send Credit-Control-Answer back to requester + */ + override fun creditControlRequest(creditControlAnswer: StreamObserver): StreamObserver { + + val streamId = newUniqueStreamId() + logger.info("Starting Credit-Control-Request with streamId: {}", streamId) + ocsAsyncRequestConsumer.putCreditControlClient(streamId, creditControlAnswer) + return StreamObserverForStreamWithId(streamId) + } + + private inner class StreamObserverForStreamWithId(private val streamId: String) : StreamObserver { + + /** + * This method gets called every time a Credit-Control-Request is received + * from the OCS. + * @param request + */ + override fun onNext(request: CreditControlRequestInfo) { + if(request.type == NONE) { + // this request is just to keep connection alive + return + } + logger.info("Received Credit-Control-Request request :: " + "for MSISDN: {} with request id: {}", + request.msisdn, request.requestId) + + ocsAsyncRequestConsumer.creditControlRequestEvent(streamId = streamId, request = request) + } + + override fun onError(t: Throwable) { + // TODO vihang: handle onError for stream observers + } + + override fun onCompleted() { + logger.info("Credit-Control-Request with streamId: {} completed", streamId) + ocsAsyncRequestConsumer.deleteCreditControlClient(streamId) + } + } + + /** + * The `ActivateRequest` does not have any fields, and so it is ignored. + * In return, the server starts to send "stream" of `ActivateResponse` + * which is actually a "request". + * + * After the connection, the first response will have empty string as MSISDN. + * It should to be ignored by OCS gateway. This method sends that empty + * response back to the invoker. + * + * @param request Is ignored. + * @param activateResponse the stream observer used to send the response back. + */ + override fun activate( + request: ActivateRequest, + activateResponse: StreamObserver) { + + val streamId = newUniqueStreamId() + logger.info("Starting Activate-Response stream with streamId: {}", streamId) + ocsAsyncRequestConsumer.updateActivateResponse(streamId, activateResponse) + + val initialDummyResponse = ActivateResponse.newBuilder().setMsisdn("").build() + activateResponse.onNext(initialDummyResponse) + } + + private fun newUniqueStreamId(): String { + return UUID.randomUUID().toString() + } +} diff --git a/ocs-ktc/src/main/kotlin/org/ostelco/prime/ocs/core/OnlineCharging.kt b/ocs-ktc/src/main/kotlin/org/ostelco/prime/ocs/core/OnlineCharging.kt new file mode 100644 index 000000000..e83e7b230 --- /dev/null +++ b/ocs-ktc/src/main/kotlin/org/ostelco/prime/ocs/core/OnlineCharging.kt @@ -0,0 +1,110 @@ +package org.ostelco.prime.ocs.core + +import io.grpc.stub.StreamObserver +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.asCoroutineDispatcher +import kotlinx.coroutines.launch +import org.ostelco.ocs.api.ActivateResponse +import org.ostelco.ocs.api.CreditControlAnswerInfo +import org.ostelco.ocs.api.CreditControlRequestInfo +import org.ostelco.ocs.api.FinalUnitAction +import org.ostelco.ocs.api.FinalUnitIndication +import org.ostelco.ocs.api.MultipleServiceCreditControl +import org.ostelco.ocs.api.ReportingReason +import org.ostelco.ocs.api.ServiceUnit +import org.ostelco.prime.module.getResource +import org.ostelco.prime.ocs.analytics.AnalyticsReporter +import org.ostelco.prime.ocs.consumption.OcsAsyncRequestConsumer +import org.ostelco.prime.ocs.notifications.Notifications +import org.ostelco.prime.storage.ClientDataSource +import java.util.concurrent.ConcurrentHashMap +import java.util.concurrent.Executors + +object OnlineCharging : OcsAsyncRequestConsumer { + + private val threadContext = Executors.newSingleThreadExecutor().asCoroutineDispatcher() + + private val ccaStreamMap = ConcurrentHashMap>() + private val activateStreamMap = ConcurrentHashMap>() + + private val storage: ClientDataSource = getResource() + + override fun putCreditControlClient( + streamId: String, + creditControlAnswer: StreamObserver) { + + ccaStreamMap[streamId] = creditControlAnswer + } + + override fun updateActivateResponse(streamId: String, activateResponse: StreamObserver) { + activateStreamMap[streamId] = activateResponse + } + + override fun deleteCreditControlClient(streamId: String) { + ccaStreamMap.remove(streamId) + } + + override fun creditControlRequestEvent(streamId: String, request: CreditControlRequestInfo) { + + val msisdn = request.msisdn + + if (msisdn != null) { + + CoroutineScope(threadContext).launch { + + val response = CreditControlAnswerInfo.newBuilder() + .setRequestId(request.requestId) + .setMsisdn(msisdn) + + if (request.msccCount > 0) { + val mscc = request.getMscc(0) + val requested = mscc?.requested?.totalOctets ?: 0 + val used = mscc?.used?.totalOctets ?: 0 + + val responseMscc = MultipleServiceCreditControl + .newBuilder(mscc) + .setValidityTime(86400) + + val (granted, balance) = storage.consume(msisdn, used, requested).fold({ Pair(0L, null) }, { it }) + + val grantedTotalOctets = if (mscc.reportingReason != ReportingReason.FINAL + && mscc.requested.totalOctets > 0) { + + if (granted < mscc.requested.totalOctets) { + responseMscc.finalUnitIndication = FinalUnitIndication.newBuilder() + .setFinalUnitAction(FinalUnitAction.TERMINATE) + .setIsSet(true) + .build() + } + + granted + + } else { + // Use -1 to indicate no granted service unit should be included in the answer + -1 + } + + responseMscc.granted = ServiceUnit.newBuilder().setTotalOctets(grantedTotalOctets).build() + + if (balance != null) { + launch { + AnalyticsReporter.report( + request = request, + bundleBytes = balance) + } + + launch { + Notifications.lowBalanceAlert( + msisdn = msisdn, + reserved = granted, + balance = balance) + } + } + response.addMscc(responseMscc) + } + + ccaStreamMap[streamId]?.onNext(response.build()) + } + } + } +} \ No newline at end of file diff --git a/ocs-ktc/src/main/kotlin/org/ostelco/prime/ocs/notifications/Notifications.kt b/ocs-ktc/src/main/kotlin/org/ostelco/prime/ocs/notifications/Notifications.kt new file mode 100644 index 000000000..90b64c45a --- /dev/null +++ b/ocs-ktc/src/main/kotlin/org/ostelco/prime/ocs/notifications/Notifications.kt @@ -0,0 +1,17 @@ +package org.ostelco.prime.ocs.notifications + +import org.ostelco.prime.appnotifier.AppNotifier +import org.ostelco.prime.module.getResource +import org.ostelco.prime.ocs.ConfigRegistry + +object Notifications { + + private val appNotifier by lazy { getResource() } + + fun lowBalanceAlert(msisdn: String, reserved: Long, balance: Long) { + val lowBalanceThreshold = ConfigRegistry.config.lowBalanceThreshold + if ((balance < lowBalanceThreshold) && ((balance + reserved) > lowBalanceThreshold)) { + appNotifier.notify(msisdn, "Pi", "You have less then " + lowBalanceThreshold / 1000000 + "Mb data left") + } + } +} \ No newline at end of file diff --git a/ocs-ktc/src/main/resources/META-INF/services/io.dropwizard.jackson.Discoverable b/ocs-ktc/src/main/resources/META-INF/services/io.dropwizard.jackson.Discoverable new file mode 100644 index 000000000..8056fe23b --- /dev/null +++ b/ocs-ktc/src/main/resources/META-INF/services/io.dropwizard.jackson.Discoverable @@ -0,0 +1 @@ +org.ostelco.prime.module.PrimeModule \ No newline at end of file diff --git a/ocs-ktc/src/main/resources/META-INF/services/org.ostelco.prime.module.PrimeModule b/ocs-ktc/src/main/resources/META-INF/services/org.ostelco.prime.module.PrimeModule new file mode 100644 index 000000000..f639024a8 --- /dev/null +++ b/ocs-ktc/src/main/resources/META-INF/services/org.ostelco.prime.module.PrimeModule @@ -0,0 +1 @@ +org.ostelco.prime.ocs.OcsModule \ No newline at end of file diff --git a/prime-modules/build.gradle b/prime-modules/build.gradle index 0824c311a..fc0195603 100644 --- a/prime-modules/build.gradle +++ b/prime-modules/build.gradle @@ -17,6 +17,7 @@ dependencies { api project(':model') api "io.dropwizard:dropwizard-core:$dropwizardVersion" + api "io.arrow-kt:arrow-core:$arrowVersion" api "io.arrow-kt:arrow-typeclasses:$arrowVersion" api "io.arrow-kt:arrow-instances-core:$arrowVersion" diff --git a/prime-modules/src/main/kotlin/org/ostelco/prime/ocs/OcsSubscriberService.kt b/prime-modules/src/main/kotlin/org/ostelco/prime/ocs/OcsSubscriberService.kt index 492d6ff6b..26cf8df32 100644 --- a/prime-modules/src/main/kotlin/org/ostelco/prime/ocs/OcsSubscriberService.kt +++ b/prime-modules/src/main/kotlin/org/ostelco/prime/ocs/OcsSubscriberService.kt @@ -2,12 +2,19 @@ package org.ostelco.prime.ocs import arrow.core.Either import org.ostelco.prime.model.Bundle +import kotlin.DeprecationLevel.ERROR +import kotlin.DeprecationLevel.WARNING +@Deprecated(message = "This service bas been discontinued", level = WARNING) interface OcsSubscriberService { + @Deprecated(message = "This service bas been discontinued", level = ERROR) fun topup(subscriberId: String, sku: String): Either } +@Deprecated(message = "This service bas been discontinued", level = WARNING) interface OcsAdminService { + @Deprecated(message = "This service bas been discontinued", level = ERROR) fun addBundle(bundle: Bundle) + @Deprecated(message = "This service bas been discontinued", level = ERROR) fun addMsisdnToBundleMapping(msisdn: String, bundleId: String) } \ No newline at end of file diff --git a/prime-modules/src/main/kotlin/org/ostelco/prime/storage/Variants.kt b/prime-modules/src/main/kotlin/org/ostelco/prime/storage/Variants.kt index e432f4097..aa248e569 100644 --- a/prime-modules/src/main/kotlin/org/ostelco/prime/storage/Variants.kt +++ b/prime-modules/src/main/kotlin/org/ostelco/prime/storage/Variants.kt @@ -1,11 +1,21 @@ package org.ostelco.prime.storage import arrow.core.Either -import org.ostelco.prime.apierror.ApiError -import org.ostelco.prime.model.* +import org.ostelco.prime.model.ApplicationToken +import org.ostelco.prime.model.Bundle +import org.ostelco.prime.model.ChangeSegment +import org.ostelco.prime.model.Offer +import org.ostelco.prime.model.Plan +import org.ostelco.prime.model.Product +import org.ostelco.prime.model.ProductClass +import org.ostelco.prime.model.PurchaseRecord +import org.ostelco.prime.model.ScanInformation +import org.ostelco.prime.model.Segment +import org.ostelco.prime.model.Subscriber +import org.ostelco.prime.model.SubscriberState +import org.ostelco.prime.model.Subscription import org.ostelco.prime.paymentprocessor.core.PaymentError import org.ostelco.prime.paymentprocessor.core.ProductInfo -import java.util.* interface ClientDocumentStore { @@ -90,6 +100,11 @@ interface ClientGraphStore { */ fun updateBundle(bundle: Bundle): Either + /** + * Set balance after OCS Topup or Consumption + */ + fun consume(msisdn: String, usedBytes: Long, requestedBytes: Long): Either> + /** * Get msisdn for the given subscription-id */ diff --git a/prime/build.gradle b/prime/build.gradle index 5642316aa..f522ac050 100644 --- a/prime/build.gradle +++ b/prime/build.gradle @@ -31,7 +31,7 @@ dependencies { implementation project(':prime-modules') // prime-modules - runtimeOnly project(':ocs') + runtimeOnly project(':ocs-ktc') runtimeOnly project(':firebase-store') runtimeOnly project(':neo4j-store') runtimeOnly project(':pseudonym-server') diff --git a/prime/infra/dev/prime.yaml b/prime/infra/dev/prime.yaml index cef58b9fb..0a7117a87 100644 --- a/prime/infra/dev/prime.yaml +++ b/prime/infra/dev/prime.yaml @@ -108,7 +108,7 @@ spec: app: prime tier: backend --- -apiVersion: extensions/v1beta1 +apiVersion: apps/v1 kind: Deployment metadata: name: prime @@ -116,7 +116,11 @@ metadata: app: prime tier: backend spec: - replicas: 1 + replicas: 3 + selector: + matchLabels: + app: prime + tier: backend template: metadata: labels: diff --git a/prime/infra/prod/prime.yaml b/prime/infra/prod/prime.yaml index a671da0f6..89ac4aa85 100644 --- a/prime/infra/prod/prime.yaml +++ b/prime/infra/prod/prime.yaml @@ -70,7 +70,7 @@ spec: app: prime tier: backend --- -apiVersion: extensions/v1beta1 +apiVersion: apps/v1 kind: Deployment metadata: name: prime @@ -78,7 +78,11 @@ metadata: app: prime tier: backend spec: - replicas: 1 + replicas: 3 + selector: + matchLabels: + app: prime + tier: backend template: metadata: labels: diff --git a/prime/src/integration-tests/kotlin/org/ostelco/prime/ocs/OcsTest.kt b/prime/src/integration-tests/kotlin/org/ostelco/prime/ocs/OcsTest.kt deleted file mode 100644 index ab6e08749..000000000 --- a/prime/src/integration-tests/kotlin/org/ostelco/prime/ocs/OcsTest.kt +++ /dev/null @@ -1,279 +0,0 @@ -package org.ostelco.prime.ocs - -import com.palantir.docker.compose.DockerComposeRule -import com.palantir.docker.compose.connection.waiting.HealthChecks -import io.grpc.ManagedChannelBuilder -import io.grpc.stub.StreamObserver -import org.apache.commons.lang3.RandomStringUtils -import org.joda.time.Duration -import org.junit.AfterClass -import org.junit.Assert.assertEquals -import org.junit.BeforeClass -import org.junit.ClassRule -import org.junit.Test -import org.ostelco.ocs.api.ActivateRequest -import org.ostelco.ocs.api.ActivateResponse -import org.ostelco.ocs.api.CreditControlAnswerInfo -import org.ostelco.ocs.api.CreditControlRequestInfo -import org.ostelco.ocs.api.CreditControlRequestType.INITIAL_REQUEST -import org.ostelco.ocs.api.MultipleServiceCreditControl -import org.ostelco.ocs.api.OcsServiceGrpc -import org.ostelco.ocs.api.OcsServiceGrpc.OcsServiceStub -import org.ostelco.ocs.api.ServiceUnit -import org.ostelco.prime.consumption.OcsGrpcServer -import org.ostelco.prime.consumption.OcsService -import org.ostelco.prime.disruptor.EventProducerImpl -import org.ostelco.prime.disruptor.OcsDisruptor -import org.ostelco.prime.getLogger -import org.ostelco.prime.storage.firebase.initFirebaseConfigRegistry -import org.ostelco.prime.storage.graph.Config -import org.ostelco.prime.storage.graph.ConfigRegistry -import org.ostelco.prime.storage.graph.Neo4jClient -import java.util.concurrent.CountDownLatch -import java.util.concurrent.TimeUnit - -/** - * - * - * This class tests the packet gateway's perspective of talking to - * the OCS over the gRPC-generated wire protocol. - */ -class OcsTest { - - private val logger by getLogger() - - abstract class AbstractObserver : StreamObserver { - - private val logger by getLogger() - - override fun onError(t: Throwable) { - // Ignore errors - } - - override fun onCompleted() { - logger.info("Completed") - } - } - - private fun newDefaultCreditControlRequestInfo(): CreditControlRequestInfo { - logger.info("Req Id: {}", REQUEST_ID) - - val mscc = MultipleServiceCreditControl.newBuilder() - .setRequested(ServiceUnit - .newBuilder() - .setTotalOctets(BYTES)) - return CreditControlRequestInfo.newBuilder() - .setType(INITIAL_REQUEST) - .setMsisdn(MSISDN) - .setRequestId(REQUEST_ID) - .addMscc(mscc) - .build() - } - - /** - * This whole test case tests the packet gateway talking to the OCS over - * the gRPC interface. - */ - @Test - fun testFetchDataRequest() { - - // If this latch reaches zero, then things went well. - val cdl = CountDownLatch(1) - - // Simulate being the OCS receiving a packet containing - // information about a data bucket containing a number - // of bytes for some MSISDN. - val requests = ocsServiceStub.creditControlRequest( - object : AbstractObserver() { - override fun onNext(response: CreditControlAnswerInfo) { - logger.info("Received answer for {}", - response.msisdn) - assertEquals(MSISDN, response.msisdn) - assertEquals(REQUEST_ID, response.requestId) - cdl.countDown() - } - }) - - // Simulate packet gateway requesting a new bucket of data by - // injecting a packet of data into the "onNext" method declared - // above. - requests.onNext(newDefaultCreditControlRequestInfo()) - - // Wait for response (max ten seconds) and the pass the test only - // if a response was actually generated (and cdl counted down to zero). - cdl.await(TIMEOUT_IN_SECONDS, TimeUnit.SECONDS) - - requests.onCompleted() - - assertEquals(0, cdl.count) - } - - /** - * Simulate sending a request to activate a subscription. - * - * @throws InterruptedException - */ - @Test - fun testActivateMsisdn() { - - val cdl = CountDownLatch(2) - - val streamObserver = object : AbstractObserver() { - override fun onNext(response: ActivateResponse) { - if (!response.msisdn.isEmpty()) { - logger.info("Activate {}", response.msisdn) - assertEquals(MSISDN, response.msisdn) - } - cdl.countDown() - } - } - - // Get the default (singelton) instance of an activation request. - val activateRequest = ActivateRequest.getDefaultInstance() - - // Send it over the wire, but also send stream observer that will be - // invoked when a reply comes back. - ocsServiceStub.activate(activateRequest, streamObserver) - - // Wait for a second to let things get through, then move on. - Thread.sleep(ONE_SECOND_IN_MILLISECONDS) - - // Send a report using the producer to the pipeline that will - // inject a PrimeEvent that will top up the data bundle balance. - producer.topupDataBundleBalanceEvent( - requestId = TOPUP_REQ_ID, - bundleId = BUNDLE_ID, - bytes = NO_OF_BYTES_TO_ADD) - - // Now wait, again, for the latch to reach zero, and fail the test - // ff it hasn't. - cdl.await(TIMEOUT_IN_SECONDS, TimeUnit.SECONDS) - assertEquals(0, cdl.count) - } - - companion object { - - /** - * The port on which the gRPC service will be receiving incoming - * connections. - */ - private const val PORT = 8082 - - /** - * The phone numbe for which we're faking data consumption during this test. - */ - private const val MSISDN = "4790300017" - - private const val BUNDLE_ID = "foo@bar.com" - - private const val TOPUP_REQ_ID = "req-id" - - // Default chunk of byte used in various test cases - private const val BYTES: Long = 100 - - // Request ID used by OCS gateway to correlate responses with requests - private val REQUEST_ID = RandomStringUtils.randomAlphanumeric(22) - - private const val NO_OF_BYTES_TO_ADD = 10000L - - private const val TIMEOUT_IN_SECONDS = 10L - - private const val ONE_SECOND_IN_MILLISECONDS = 1000L - - /** - * This is the "disruptor" (processing engine) that will process - * PrimeEvent instances and send them through a sequence of operations that - * will update the balance of bytes for a particular subscription. - * - * - * Disruptor also provides RingBuffer, which is used by Producer - */ - private lateinit var disruptor: OcsDisruptor - - /** - * - */ - private lateinit var producer: EventProducerImpl - - /** - * The gRPC service that will produce incoming events from the - * simulated packet gateway, contains an [OcsSubscriberService] instance bound - * to a particular port (in our case 8082). - */ - private lateinit var ocsServer: OcsGrpcServer - - /** - * The "sub" that will mediate access to the GRPC channel, - * providing an API to send / receive data to/from it. - */ - private lateinit var ocsServiceStub: OcsServiceStub - - @ClassRule - @JvmField - var docker: DockerComposeRule = DockerComposeRule.builder() - .file("src/integration-tests/resources/docker-compose.yaml") - .waitingForService("neo4j", HealthChecks.toHaveAllPortsOpen()) - .waitingForService("neo4j", - HealthChecks.toRespond2xxOverHttp(7474) { - port -> port.inFormat("http://\$HOST:\$EXTERNAL_PORT/browser") - }, - Duration.standardSeconds(40L)) - .build() - - @BeforeClass - @JvmStatic - fun setUp() { - ConfigRegistry.config = Config().apply { - this.host = "0.0.0.0" - this.protocol = "bolt" - } - initFirebaseConfigRegistry() - - Neo4jClient.start() - - // Set up processing pipeline - disruptor = OcsDisruptor() - producer = EventProducerImpl(disruptor.disruptor.ringBuffer) - - // Set up the gRPC server at a particular port with a particular - // service, that is connected to the processing pipeline. - val ocsService = OcsService(producer) - ocsServer = OcsGrpcServer(PORT, ocsService.ocsGrpcService) - - val ocsState = OcsState() - ocsState.msisdnToBundleIdMap[MSISDN] = BUNDLE_ID - ocsState.bundleIdToMsisdnMap[BUNDLE_ID] = mutableSetOf(MSISDN) - - ocsState.addDataBundleBytesForMsisdn(MSISDN, NO_OF_BYTES_TO_ADD) - - // Events flow: - // Producer:(OcsService, Subscriber) - // -> Handler:(OcsState) - // -> Handler:(OcsService, Subscriber) - disruptor.disruptor.handleEventsWith(ocsState).then(ocsService.eventHandler) - - // start disruptor and ocs services. - disruptor.start() - ocsServer.start() - - // Set up a channel to be used to communicate as an OCS instance, to an - // Prime instance. - val channel = ManagedChannelBuilder - .forTarget("0.0.0.0:$PORT") - .usePlaintext() // disable encryption for testing - .build() - - // Initialize the stub that will be used to actually - // communicate from the client emulating being the OCS. - ocsServiceStub = OcsServiceGrpc.newStub(channel) - } - - @AfterClass - @JvmStatic - fun tearDown() { - disruptor.stop() - ocsServer.forceStop() - Neo4jClient.stop() - } - } -} diff --git a/prime/src/integration-tests/kotlin/org/ostelco/prime/storage/graph/Neo4jStorageTest.kt b/prime/src/integration-tests/kotlin/org/ostelco/prime/storage/graph/Neo4jStorageTest.kt index 2dd16ae3d..a8ffea92b 100644 --- a/prime/src/integration-tests/kotlin/org/ostelco/prime/storage/graph/Neo4jStorageTest.kt +++ b/prime/src/integration-tests/kotlin/org/ostelco/prime/storage/graph/Neo4jStorageTest.kt @@ -13,12 +13,9 @@ import org.junit.Before import org.junit.BeforeClass import org.junit.ClassRule import org.junit.Test -import org.mockito.Mockito.mock -import org.ostelco.prime.disruptor.EventProducer import org.ostelco.prime.model.Bundle import org.ostelco.prime.model.PurchaseRecord import org.ostelco.prime.model.Subscriber -import org.ostelco.prime.ocs.OcsPrimeServiceSingleton import org.ostelco.prime.storage.GraphStore import org.ostelco.prime.storage.firebase.initFirebaseConfigRegistry import org.ostelco.prime.storage.graph.Products.DATA_TOPUP_3GB @@ -122,8 +119,6 @@ class Neo4jStorageTest { Neo4jClient.start() initDatabase() - - OcsPrimeServiceSingleton.init(mock(EventProducer::class.java)) } @JvmStatic diff --git a/settings.gradle b/settings.gradle index 0b1c86fcd..5e1830421 100644 --- a/settings.gradle +++ b/settings.gradle @@ -21,6 +21,7 @@ include ':neo4j-admin-tools' include ':neo4j-store' include ':ocs' include ':ocs-grpc-api' +include ':ocs-ktc' include ':ocsgw' include ':ostelco-lib' include ':payment-processor' @@ -53,6 +54,7 @@ project(':neo4j-admin-tools').projectDir = "$rootDir/tools/neo4j-admin-tools" as project(':neo4j-store').projectDir = "$rootDir/neo4j-store" as File project(':ocs').projectDir = "$rootDir/ocs" as File project(':ocs-grpc-api').projectDir = "$rootDir/ocs-grpc-api" as File +project(':ocs-ktc').projectDir = "$rootDir/ocs-ktc" as File project(':ocsgw').projectDir = "$rootDir/ocsgw" as File project(':ostelco-lib').projectDir = "$rootDir/ostelco-lib" as File project(':payment-processor').projectDir = "$rootDir/payment-processor" as File From e0b2f8cecab0161d37dfeb5ad82c87490a195a6b Mon Sep 17 00:00:00 2001 From: Vihang Patil Date: Thu, 20 Dec 2018 11:46:20 +0100 Subject: [PATCH 2/2] Updated dependency versions. --- README.md | 2 +- acceptance-tests/build.gradle | 2 +- .../kotlin/org/ostelco/at/common/TestUser.kt | 2 +- admin-api/build.gradle | 2 +- analytics-module/build.gradle | 4 +- app-notifier/build.gradle | 2 +- auth-server/build.gradle | 2 +- bq-metrics-extractor/build.gradle | 4 +- build.gradle | 40 +++++++++---------- client-api/build.gradle | 2 +- dataflow-pipelines/build.gradle | 2 +- diameter-stack/build.gradle | 2 +- diameter-test/build.gradle | 2 +- ext-auth-provider/build.gradle | 2 +- firebase-extensions/build.gradle | 2 +- firebase-store/build.gradle | 2 +- graphql/build.gradle | 2 +- imei-lookup/build.gradle | 2 +- jersey/build.gradle | 2 +- model/build.gradle | 2 +- neo4j-store/build.gradle | 2 +- .../ostelco/prime/storage/graph/Neo4jStore.kt | 2 +- ocs-ktc/build.gradle | 2 +- ocs/build.gradle | 2 +- payment-processor/build.gradle | 2 +- prime-client-api/build.gradle | 4 +- prime-modules/build.gradle | 2 +- prime/build.gradle | 4 +- pseudonym-server/build.gradle | 2 +- sim-administration/pom.xml | 2 +- slack/build.gradle | 2 +- tools/neo4j-admin-tools/build.gradle | 2 +- 32 files changed, 55 insertions(+), 55 deletions(-) diff --git a/README.md b/README.md index 5b6492204..f8adef67a 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ -[![Kotlin version badge](https://img.shields.io/badge/kotlin-1.3.10-blue.svg)](http://kotlinlang.org/) +[![Kotlin version badge](https://img.shields.io/badge/kotlin-1.3.11-blue.svg)](http://kotlinlang.org/) [![Prime version](https://img.shields.io/github/tag/ostelco/ostelco-core.svg)](https://github.com/ostelco/ostelco-core/tags) [![GitHub license](https://img.shields.io/github/license/ostelco/ostelco-core.svg)](https://github.com/ostelco/ostelco-core/blob/master/LICENSE) diff --git a/acceptance-tests/build.gradle b/acceptance-tests/build.gradle index 047de7db5..38b76509b 100644 --- a/acceptance-tests/build.gradle +++ b/acceptance-tests/build.gradle @@ -1,5 +1,5 @@ plugins { - id "org.jetbrains.kotlin.jvm" version "1.3.10" + id "org.jetbrains.kotlin.jvm" version "1.3.11" id "application" id "com.github.johnrengelman.shadow" version "4.0.3" } diff --git a/acceptance-tests/src/main/kotlin/org/ostelco/at/common/TestUser.kt b/acceptance-tests/src/main/kotlin/org/ostelco/at/common/TestUser.kt index 3a278d77b..65e958769 100644 --- a/acceptance-tests/src/main/kotlin/org/ostelco/at/common/TestUser.kt +++ b/acceptance-tests/src/main/kotlin/org/ostelco/at/common/TestUser.kt @@ -38,4 +38,4 @@ fun createSubscription(email: String): String { } private val random = Random() -fun randomInt(): Int = random.nextInt(999) \ No newline at end of file +fun randomInt(): Int = random.nextInt(99999) \ No newline at end of file diff --git a/admin-api/build.gradle b/admin-api/build.gradle index d32848ded..ef493a5a5 100644 --- a/admin-api/build.gradle +++ b/admin-api/build.gradle @@ -1,5 +1,5 @@ plugins { - id "org.jetbrains.kotlin.jvm" version "1.3.10" + id "org.jetbrains.kotlin.jvm" version "1.3.11" id "java-library" } diff --git a/analytics-module/build.gradle b/analytics-module/build.gradle index 7c3a073f2..90dc28b42 100644 --- a/analytics-module/build.gradle +++ b/analytics-module/build.gradle @@ -1,5 +1,5 @@ plugins { - id "org.jetbrains.kotlin.jvm" version "1.3.10" + id "org.jetbrains.kotlin.jvm" version "1.3.11" id "java-library" } @@ -9,5 +9,5 @@ dependencies { implementation "com.google.cloud:google-cloud-pubsub:$googleCloudVersion" implementation 'com.google.code.gson:gson:2.8.5' - testImplementation 'com.google.api:gax-grpc:1.35.0' + testImplementation 'com.google.api:gax-grpc:1.35.1' } diff --git a/app-notifier/build.gradle b/app-notifier/build.gradle index 850f8b624..35cdba153 100644 --- a/app-notifier/build.gradle +++ b/app-notifier/build.gradle @@ -1,5 +1,5 @@ plugins { - id "org.jetbrains.kotlin.jvm" version "1.3.10" + id "org.jetbrains.kotlin.jvm" version "1.3.11" id "java-library" } diff --git a/auth-server/build.gradle b/auth-server/build.gradle index a014ba51f..092c093f7 100644 --- a/auth-server/build.gradle +++ b/auth-server/build.gradle @@ -1,5 +1,5 @@ plugins { - id "org.jetbrains.kotlin.jvm" version "1.3.10" + id "org.jetbrains.kotlin.jvm" version "1.3.11" id "application" id "com.github.johnrengelman.shadow" version "4.0.3" id "idea" diff --git a/bq-metrics-extractor/build.gradle b/bq-metrics-extractor/build.gradle index 8d6f2dc06..581b68526 100644 --- a/bq-metrics-extractor/build.gradle +++ b/bq-metrics-extractor/build.gradle @@ -3,7 +3,7 @@ buildscript { } plugins { - id "org.jetbrains.kotlin.jvm" version "1.3.10" + id "org.jetbrains.kotlin.jvm" version "1.3.11" id "application" id "com.github.johnrengelman.shadow" version "4.0.3" id "idea" @@ -21,7 +21,7 @@ dependencies { implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlinVersion" implementation "com.google.cloud:google-cloud-bigquery:$googleCloudVersion" - implementation 'io.prometheus:simpleclient_pushgateway:0.5.0' + implementation 'io.prometheus:simpleclient_pushgateway:0.6.0' runtimeOnly "io.dropwizard:dropwizard-json-logging:$dropwizardVersion" diff --git a/build.gradle b/build.gradle index e6e0343aa..da55e6cc2 100644 --- a/build.gradle +++ b/build.gradle @@ -34,36 +34,36 @@ subprojects { ext { assertJVersion = "3.11.1" arrowVersion = "0.8.1" - beamVersion = "2.8.0" + beamVersion = "2.9.0" dropwizardVersion = "1.3.7" - firebaseVersion = "6.5.0" - googleCloudVersion = "1.53.0" - grpcVersion = "1.16.1" + firebaseVersion = "6.6.0" + googleCloudVersion = "1.56.0" + grpcVersion = "1.17.1" guavaVersion = "27.0.1-jre" - jacksonVersion = "2.9.7" + jacksonVersion = "2.9.8" javaxActivationVersion = "1.1.1" // Keeping it version 1.16.1 to be consistent with grpc via PubSub client lib // Keeping it version 1.16.1 to be consistent with netty via Firebase lib jaxbVersion = "2.3.1" junit5Version = "5.3.2" - kotlinVersion = "1.3.10" + kotlinVersion = "1.3.11" kotlinXCoroutinesVersion = "1.0.1" mockitoVersion = "2.23.4" - neo4jDriverVersion="1.7.1" + neo4jDriverVersion="1.7.2" neo4jVersion="3.5.0" - stripeVersion = "7.8.0" + stripeVersion = "7.10.0" } } -//dependencyUpdates.resolutionStrategy { -// componentSelection { rules -> -// rules.all { ComponentSelection selection -> -// boolean rejected = ['alpha', 'beta', 'rc', 'cr', 'm', 'redhat'].any { qualifier -> -// selection.candidate.version ==~ /(?i).*[.-]${qualifier}[.\d-]*/ -// } -// if (rejected) { -// selection.reject('Release candidate') -// } -// } -// } -//} \ No newline at end of file +dependencyUpdates.resolutionStrategy { + componentSelection { rules -> + rules.all { ComponentSelection selection -> + boolean rejected = ['alpha', 'beta', 'rc', 'cr', 'm', 'redhat'].any { qualifier -> + selection.candidate.version ==~ /(?i).*[.-]${qualifier}[.\d-]*/ + } + if (rejected) { + selection.reject('Release candidate') + } + } + } +} \ No newline at end of file diff --git a/client-api/build.gradle b/client-api/build.gradle index 553481240..57278710e 100644 --- a/client-api/build.gradle +++ b/client-api/build.gradle @@ -1,5 +1,5 @@ plugins { - id "org.jetbrains.kotlin.jvm" version "1.3.10" + id "org.jetbrains.kotlin.jvm" version "1.3.11" id "java-library" } diff --git a/dataflow-pipelines/build.gradle b/dataflow-pipelines/build.gradle index 60f8ba8f3..dcc6fbd1d 100644 --- a/dataflow-pipelines/build.gradle +++ b/dataflow-pipelines/build.gradle @@ -1,5 +1,5 @@ plugins { - id "org.jetbrains.kotlin.jvm" version "1.3.10" + id "org.jetbrains.kotlin.jvm" version "1.3.11" id "application" id "com.github.johnrengelman.shadow" version "4.0.3" id "idea" diff --git a/diameter-stack/build.gradle b/diameter-stack/build.gradle index 49e7e3273..8848b733e 100644 --- a/diameter-stack/build.gradle +++ b/diameter-stack/build.gradle @@ -1,5 +1,5 @@ plugins { - id "org.jetbrains.kotlin.jvm" version "1.3.10" + id "org.jetbrains.kotlin.jvm" version "1.3.11" id "java-library" id "signing" id "maven" diff --git a/diameter-test/build.gradle b/diameter-test/build.gradle index ea57e1741..acae0223a 100644 --- a/diameter-test/build.gradle +++ b/diameter-test/build.gradle @@ -1,5 +1,5 @@ plugins { - id "org.jetbrains.kotlin.jvm" version "1.3.10" + id "org.jetbrains.kotlin.jvm" version "1.3.11" id "java-library" id "signing" id "maven" diff --git a/ext-auth-provider/build.gradle b/ext-auth-provider/build.gradle index 4f1bd7a5f..eb4e4d800 100644 --- a/ext-auth-provider/build.gradle +++ b/ext-auth-provider/build.gradle @@ -1,5 +1,5 @@ plugins { - id "org.jetbrains.kotlin.jvm" version "1.3.10" + id "org.jetbrains.kotlin.jvm" version "1.3.11" id "application" id "com.github.johnrengelman.shadow" version "4.0.3" } diff --git a/firebase-extensions/build.gradle b/firebase-extensions/build.gradle index 95e9d870b..4ef9311a6 100644 --- a/firebase-extensions/build.gradle +++ b/firebase-extensions/build.gradle @@ -1,5 +1,5 @@ plugins { - id "org.jetbrains.kotlin.jvm" version "1.3.10" + id "org.jetbrains.kotlin.jvm" version "1.3.11" id "java-library" } diff --git a/firebase-store/build.gradle b/firebase-store/build.gradle index 5d05ddc4a..5ab31e234 100644 --- a/firebase-store/build.gradle +++ b/firebase-store/build.gradle @@ -1,5 +1,5 @@ plugins { - id "org.jetbrains.kotlin.jvm" version "1.3.10" + id "org.jetbrains.kotlin.jvm" version "1.3.11" id "java-library" } diff --git a/graphql/build.gradle b/graphql/build.gradle index 570a2c0de..8e65b0b09 100644 --- a/graphql/build.gradle +++ b/graphql/build.gradle @@ -1,5 +1,5 @@ plugins { - id "org.jetbrains.kotlin.jvm" version "1.3.10" + id "org.jetbrains.kotlin.jvm" version "1.3.11" id "java-library" } diff --git a/imei-lookup/build.gradle b/imei-lookup/build.gradle index 401356457..e62ba6bfc 100644 --- a/imei-lookup/build.gradle +++ b/imei-lookup/build.gradle @@ -1,5 +1,5 @@ plugins { - id "org.jetbrains.kotlin.jvm" version "1.3.10" + id "org.jetbrains.kotlin.jvm" version "1.3.11" id "java-library" id "idea" } diff --git a/jersey/build.gradle b/jersey/build.gradle index 52f2cc6d3..0c5129ea1 100644 --- a/jersey/build.gradle +++ b/jersey/build.gradle @@ -1,5 +1,5 @@ plugins { - id "org.jetbrains.kotlin.jvm" version "1.3.10" + id "org.jetbrains.kotlin.jvm" version "1.3.11" id "java-library" } diff --git a/model/build.gradle b/model/build.gradle index 70f207c81..e4507b3f0 100644 --- a/model/build.gradle +++ b/model/build.gradle @@ -1,5 +1,5 @@ plugins { - id "org.jetbrains.kotlin.jvm" version "1.3.10" + id "org.jetbrains.kotlin.jvm" version "1.3.11" id "java-library" } diff --git a/neo4j-store/build.gradle b/neo4j-store/build.gradle index 7664f1207..318630c2a 100644 --- a/neo4j-store/build.gradle +++ b/neo4j-store/build.gradle @@ -1,5 +1,5 @@ plugins { - id "org.jetbrains.kotlin.jvm" version "1.3.10" + id "org.jetbrains.kotlin.jvm" version "1.3.11" id "java-library" } diff --git a/neo4j-store/src/main/kotlin/org/ostelco/prime/storage/graph/Neo4jStore.kt b/neo4j-store/src/main/kotlin/org/ostelco/prime/storage/graph/Neo4jStore.kt index 0f98b292c..8d84d31e5 100644 --- a/neo4j-store/src/main/kotlin/org/ostelco/prime/storage/graph/Neo4jStore.kt +++ b/neo4j-store/src/main/kotlin/org/ostelco/prime/storage/graph/Neo4jStore.kt @@ -472,7 +472,7 @@ object Neo4jStoreSingleton : GraphStore { return getProduct(subscriberId, sku).fold( { - Either.left(org.ostelco.prime.paymentprocessor.core.NotFoundError("Product ${sku} is unavailable", + Either.left(org.ostelco.prime.paymentprocessor.core.NotFoundError("Product $sku is unavailable", error = it)) }, { diff --git a/ocs-ktc/build.gradle b/ocs-ktc/build.gradle index 46aae68e6..61442bcab 100644 --- a/ocs-ktc/build.gradle +++ b/ocs-ktc/build.gradle @@ -1,5 +1,5 @@ plugins { - id "org.jetbrains.kotlin.jvm" version "1.3.10" + id "org.jetbrains.kotlin.jvm" version "1.3.11" id "java-library" } diff --git a/ocs/build.gradle b/ocs/build.gradle index 5da0f1627..d9a0e5240 100644 --- a/ocs/build.gradle +++ b/ocs/build.gradle @@ -1,5 +1,5 @@ plugins { - id "org.jetbrains.kotlin.jvm" version "1.3.10" + id "org.jetbrains.kotlin.jvm" version "1.3.11" id "java-library" } diff --git a/payment-processor/build.gradle b/payment-processor/build.gradle index 2dadff728..ea441aaca 100644 --- a/payment-processor/build.gradle +++ b/payment-processor/build.gradle @@ -1,5 +1,5 @@ plugins { - id "org.jetbrains.kotlin.jvm" version "1.3.10" + id "org.jetbrains.kotlin.jvm" version "1.3.11" id "java-library" id "idea" } diff --git a/prime-client-api/build.gradle b/prime-client-api/build.gradle index f6b63bb55..50d2b3a00 100644 --- a/prime-client-api/build.gradle +++ b/prime-client-api/build.gradle @@ -1,6 +1,6 @@ plugins { id 'java-library' - id 'org.hidetake.swagger.generator' version '2.15.0' + id 'org.hidetake.swagger.generator' version '2.15.1' id "idea" } @@ -21,7 +21,7 @@ sourceSets.main.resources.srcDir "${swaggerSources.'java-client'.code.outputDir} dependencies { // implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlinVersion" - swaggerCodegen 'io.swagger:swagger-codegen-cli:2.3.1' + swaggerCodegen 'io.swagger:swagger-codegen-cli:2.4.0' implementation 'javax.annotation:javax.annotation-api:1.3.2' diff --git a/prime-modules/build.gradle b/prime-modules/build.gradle index fc0195603..ab9bd8dcb 100644 --- a/prime-modules/build.gradle +++ b/prime-modules/build.gradle @@ -1,5 +1,5 @@ plugins { - id "org.jetbrains.kotlin.jvm" version "1.3.10" + id "org.jetbrains.kotlin.jvm" version "1.3.11" id "java-library" } diff --git a/prime/build.gradle b/prime/build.gradle index f522ac050..de4620910 100644 --- a/prime/build.gradle +++ b/prime/build.gradle @@ -1,5 +1,5 @@ plugins { - id "org.jetbrains.kotlin.jvm" version "1.3.10" + id "org.jetbrains.kotlin.jvm" version "1.3.11" id "application" id "com.github.johnrengelman.shadow" version "4.0.3" id "idea" @@ -18,7 +18,7 @@ sourceSets { } } -version = "1.20.0" +version = "1.21.0" repositories { maven { diff --git a/pseudonym-server/build.gradle b/pseudonym-server/build.gradle index c99e29fea..a33ed458b 100644 --- a/pseudonym-server/build.gradle +++ b/pseudonym-server/build.gradle @@ -1,5 +1,5 @@ plugins { - id "org.jetbrains.kotlin.jvm" version "1.3.10" + id "org.jetbrains.kotlin.jvm" version "1.3.11" id "java-library" } diff --git a/sim-administration/pom.xml b/sim-administration/pom.xml index 2fe2e9264..49d2ca3be 100644 --- a/sim-administration/pom.xml +++ b/sim-administration/pom.xml @@ -28,7 +28,7 @@ UTF-8 UTF-8 1.3.7 - 1.3.10 + 1.3.11 1.8 1.8 true diff --git a/slack/build.gradle b/slack/build.gradle index 6785be750..2932ffbf5 100644 --- a/slack/build.gradle +++ b/slack/build.gradle @@ -1,5 +1,5 @@ plugins { - id "org.jetbrains.kotlin.jvm" version "1.3.10" + id "org.jetbrains.kotlin.jvm" version "1.3.11" id "java-library" } diff --git a/tools/neo4j-admin-tools/build.gradle b/tools/neo4j-admin-tools/build.gradle index e4d046771..07c0f072d 100644 --- a/tools/neo4j-admin-tools/build.gradle +++ b/tools/neo4j-admin-tools/build.gradle @@ -1,5 +1,5 @@ plugins { - id "org.jetbrains.kotlin.jvm" version "1.3.10" + id "org.jetbrains.kotlin.jvm" version "1.3.11" id "application" id "com.github.johnrengelman.shadow" version "4.0.3" id "idea"