diff --git a/.circleci/config.yml b/.circleci/config.yml index 6e090a086..3827cfa45 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -6,7 +6,7 @@ jobs: machine: image: ubuntu-1604:201903-01 environment: - JAVA_HOME: /usr/lib/jvm/zulu12.3.11-ca-jdk12.0.2-linux_x64 + JAVA_HOME: /usr/lib/jvm/zulu13.27.9-ca-jdk13-linux_x64 steps: - checkout @@ -14,8 +14,8 @@ jobs: name: upgrading Java to open-jdk-12 command: | # sudo apt update; sudo apt install -y wget - sudo wget https://cdn.azul.com/zulu/bin/zulu12.3.11-ca-jdk12.0.2-linux_x64.tar.gz -O /tmp/zulu12.3.11-ca-jdk12.0.2-linux_x64.tar.gz - sudo tar -zxf /tmp/zulu12.3.11-ca-jdk12.0.2-linux_x64.tar.gz -C /usr/lib/jvm + sudo wget https://cdn.azul.com/zulu/bin/zulu13.27.9-ca-jdk13-linux_x64.tar.gz -O /tmp/zulu13.27.9-ca-jdk13-linux_x64.tar.gz + sudo tar -zxf /tmp/zulu13.27.9-ca-jdk13-linux_x64.tar.gz -C /usr/lib/jvm - run: # checking for merge conflicts and merging locally if none exist name: merging ${CIRCLE_BRANCH} into develop locally @@ -29,7 +29,7 @@ jobs: - run: name: javac -version command: | - export PATH=/usr/lib/jvm/zulu12.3.11-ca-jdk12.0.2-linux_x64/bin:$PATH + export PATH=/usr/lib/jvm/zulu13.27.9-ca-jdk13-linux_x64/bin:$PATH javac -version - run: name: Pulling Gradle cache @@ -115,7 +115,7 @@ jobs: machine: image: ubuntu-1604:201903-01 environment: - JAVA_HOME: /usr/lib/jvm/zulu12.3.11-ca-jdk12.0.2-linux_x64 + JAVA_HOME: /usr/lib/jvm/zulu13.27.9-ca-jdk13-linux_x64 steps: - checkout @@ -124,8 +124,8 @@ jobs: command: | # sudo apt update; sudo apt install -y wget - sudo wget https://cdn.azul.com/zulu/bin/zulu12.3.11-ca-jdk12.0.2-linux_x64.tar.gz -O /tmp/zulu12.3.11-ca-jdk12.0.2-linux_x64.tar.gz - sudo tar -zxf /tmp/zulu12.3.11-ca-jdk12.0.2-linux_x64.tar.gz -C /usr/lib/jvm + sudo wget https://cdn.azul.com/zulu/bin/zulu13.27.9-ca-jdk13-linux_x64.tar.gz -O /tmp/zulu13.27.9-ca-jdk13-linux_x64.tar.gz + sudo tar -zxf /tmp/zulu13.27.9-ca-jdk13-linux_x64.tar.gz -C /usr/lib/jvm - run: name: Pulling Gradle cache command: | @@ -140,7 +140,7 @@ jobs: - run: name: javac -version command: | - export PATH=/usr/lib/jvm/zulu12.3.11-ca-jdk12.0.2-linux_x64/bin:$PATH + export PATH=/usr/lib/jvm/zulu13.27.9-ca-jdk13-linux_x64/bin:$PATH javac -version - run: name: Gradle Build Prime and ScanInfo Shredder diff --git a/.circleci/prime-dev-values.yaml b/.circleci/prime-dev-values.yaml index a06bdbd14..a8f090b2c 100644 --- a/.circleci/prime-dev-values.yaml +++ b/.circleci/prime-dev-values.yaml @@ -150,8 +150,10 @@ prime: cpu: 100m memory: 768Mi livenessProbe: {} - readinessProbe: {} - annotations: + readinessProbe: + path: /ping + port: 8080 + annotations: prometheus.io/scrape: 'true' prometheus.io/path: '/prometheus-metrics' prometheus.io/port: '8081' diff --git a/.circleci/prime-prod-values.yaml b/.circleci/prime-prod-values.yaml index da4f2ed96..ac6da9416 100644 --- a/.circleci/prime-prod-values.yaml +++ b/.circleci/prime-prod-values.yaml @@ -150,7 +150,9 @@ prime: cpu: 300m memory: 1Gi livenessProbe: {} - readinessProbe: {} + readinessProbe: + path: /ping + port: 8080 annotations: prometheus.io/scrape: 'true' prometheus.io/path: '/prometheus-metrics' diff --git a/.travis.yml b/.travis.yml index 2125fa561..393cc864f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -10,7 +10,7 @@ cache: install: echo "skip 'gradle assemble' step" -jdk: openjdk12 +jdk: openjdk13 # TODO vihang: fix neo4j-store:test script: diff --git a/acceptance-tests/Dockerfile b/acceptance-tests/Dockerfile index b0c1a8645..bdbbb3dd3 100644 --- a/acceptance-tests/Dockerfile +++ b/acceptance-tests/Dockerfile @@ -1,4 +1,4 @@ -FROM azul/zulu-openjdk:12.0.2 +FROM azul/zulu-openjdk:13 LABEL maintainer="dev@redotter.sg" diff --git a/acceptance-tests/src/main/kotlin/org/ostelco/at/common/StripePayment.kt b/acceptance-tests/src/main/kotlin/org/ostelco/at/common/StripePayment.kt index 1ff45beb9..5d6a9bd3f 100644 --- a/acceptance-tests/src/main/kotlin/org/ostelco/at/common/StripePayment.kt +++ b/acceptance-tests/src/main/kotlin/org/ostelco/at/common/StripePayment.kt @@ -75,6 +75,9 @@ object StripePayment { return token.card.id } + val MAX_TRIES = 3 + val WAIT_DELAY = 300L + /** * Obtains 'default source' directly from Stripe. Use in tests to * verify that the correspondng 'setDefaultSource' API works as @@ -85,8 +88,17 @@ object StripePayment { // https://stripe.com/docs/api/java#create_source Stripe.apiKey = System.getenv("STRIPE_API_KEY") - val customer = Customer.retrieve(stripeCustomerId) - return customer.defaultSource + var error = Exception() + + (0..MAX_TRIES).forEach { + try { + return Customer.retrieve(stripeCustomerId).defaultSource + } catch (e: Exception) { + error = e + } + } + + throw(error) } /** @@ -96,7 +108,15 @@ object StripePayment { // https://stripe.com/docs/api/java#create_card_token Stripe.apiKey = System.getenv("STRIPE_API_KEY") - val customers = Customer.list(emptyMap()).data + var customers: List = emptyList() + + (0..MAX_TRIES).forEach { + customers = Customer.list(emptyMap()).data + if (!customers.isEmpty()) + return@forEach + Thread.sleep(WAIT_DELAY) + } + return customers.first { it.id == customerId }.id } diff --git a/acceptance-tests/src/main/kotlin/org/ostelco/at/jersey/Tests.kt b/acceptance-tests/src/main/kotlin/org/ostelco/at/jersey/Tests.kt index 7fed150b1..72cdaad53 100644 --- a/acceptance-tests/src/main/kotlin/org/ostelco/at/jersey/Tests.kt +++ b/acceptance-tests/src/main/kotlin/org/ostelco/at/jersey/Tests.kt @@ -31,6 +31,7 @@ import org.ostelco.prime.customer.model.Region import org.ostelco.prime.customer.model.RegionDetails import org.ostelco.prime.customer.model.RegionDetails.StatusEnum.APPROVED import org.ostelco.prime.customer.model.RegionDetails.StatusEnum.PENDING +import org.ostelco.prime.customer.model.RegionDetails.StatusEnum.AVAILABLE import org.ostelco.prime.customer.model.RegionDetailsList import org.ostelco.prime.customer.model.ScanInformation import org.ostelco.prime.customer.model.SimProfile @@ -54,7 +55,7 @@ class CustomerTest { @Test fun `jersey test - encoded email GET and PUT customer`() { - val email ="customer-${randomInt()}+test@test.com" + val email = "customer-${randomInt()}+test@test.com" val nickname = "Test Customer" var customerId = "" try { @@ -82,7 +83,7 @@ class CustomerTest { assertEquals(createdCustomer.referralId, customer.referralId, "Incorrect 'referralId' in fetched customer") val newName = "New name: Test Customer" - val email2 ="customer-${randomInt()}.abc+test@test.com" + val email2 = "customer-${randomInt()}.abc+test@test.com" val updatedCustomer: Customer = put { path = "/customer" @@ -102,7 +103,7 @@ class CustomerTest { @Test fun `jersey test - GET and PUT customer`() { - val email ="customer-${randomInt()}+test@test.com" + val email = "customer-${randomInt()}+test@test.com" val nickname = "Test Customer" var customerId = "" try { @@ -192,8 +193,9 @@ class RegionsTest { path = "/regions" this.email = email } - - assertTrue(regionDetailsList.isEmpty(), "RegionDetails list for new customer should be empty") + regionDetailsList.forEach { + assertTrue(it.status == AVAILABLE, "All regions should be in available state") + } } finally { StripePayment.deleteCustomer(customerId = customerId) } @@ -208,12 +210,19 @@ class RegionsTest { customerId = createCustomer(name = "Test Single Region User", email = email).id enableRegion(email = email) - val regionDetailsList: Collection = get { + val regionDetailsList: RegionDetailsList = get { path = "/regions" this.email = email } - assertEquals(1, regionDetailsList.size, "Customer should have one region") + val naRegionDetails = regionDetailsList.singleOrNull { it.status != AVAILABLE } + assertTrue(naRegionDetails != null, "List should contain only one region in a state other than available") + + val noRegionDetails = regionDetailsList.singleOrNull { it.region.id == "no" } + assertTrue(noRegionDetails != null, "List should contain contain 'no' region") + + assertEquals(naRegionDetails, noRegionDetails, "RegionDetails do not match") + val regionDetails = RegionDetails() .region(Region().id("no").name("Norway")) @@ -221,7 +230,7 @@ class RegionsTest { .kycStatusMap(mapOf(KycType.JUMIO.name to KycStatus.APPROVED)) .simProfiles(SimProfileList()) - assertEquals(regionDetails, regionDetailsList.single(), "RegionDetails do not match") + assertEquals(regionDetails, noRegionDetails, "RegionDetails do not match") } finally { StripePayment.deleteCustomer(customerId = customerId) } @@ -729,9 +738,8 @@ class PurchaseTest { assert(Instant.now().toEpochMilli() - purchaseRecords.last().timestamp < 10_000) { "Missing Purchase Record" } assertEquals(expectedProducts().first(), purchaseRecords.last().product, "Incorrect 'Product' in purchase record") - val encodedEmail = URLEncoder.encode(email, StandardCharsets.UTF_8) val refundedProduct = put { - path = "/support/refund/$encodedEmail" + path = "/support/refund/$customerId" this.email = email queryParams = mapOf( "purchaseRecordId" to purchaseRecords.last().id, @@ -823,18 +831,20 @@ class JumioKycTest { } assertNotNull(scanInfo.scanId, message = "Failed to get new scanId") - val regionDetails = get { + val regionDetailsList = get { path = "/regions" this.email = email - }.single() + } - assertEquals(Region().id("no").name("Norway"), regionDetails.region) - assertEquals(PENDING, regionDetails.status, message = "Wrong State") + val noRegionDetails = regionDetailsList.singleOrNull { it.region.id == "no" } + assertTrue(noRegionDetails != null, "Did not find Norway region") + + assertEquals(PENDING, noRegionDetails.status, message = "Wrong State") assertEquals( expected = mapOf( KycType.JUMIO.name to KycStatus.PENDING), - actual = regionDetails.kycStatusMap) + actual = noRegionDetails.kycStatusMap) } finally { StripePayment.deleteCustomer(customerId = customerId) @@ -873,18 +883,22 @@ class JumioKycTest { body = dataMap } - val regionDetails = get> { + val regionDetailsList = get { path = "/regions" this.email = email - }.single() + } - assertEquals(Region().id("no").name("Norway"), regionDetails.region) - assertEquals(PENDING, regionDetails.status, message = "Wrong State") + + val noRegionDetails = regionDetailsList.singleOrNull { it.region.id == "no" } + assertTrue(noRegionDetails != null, "Did not find Norway region") + + assertEquals(Region().id("no").name("Norway"), noRegionDetails.region) + assertEquals(PENDING, noRegionDetails.status, message = "Wrong State") assertEquals( expected = mapOf( KycType.JUMIO.name to KycStatus.REJECTED), - actual = regionDetails.kycStatusMap) + actual = noRegionDetails.kycStatusMap) } finally { StripePayment.deleteCustomer(customerId = customerId) @@ -925,18 +939,21 @@ class JumioKycTest { body = dataMap } - val regionDetails = get> { + val regionDetailsList = get { path = "/regions" this.email = email - }.single() + } - assertEquals(Region().id("no").name("Norway"), regionDetails.region) - assertEquals(APPROVED, regionDetails.status, message = "Wrong State") + val noRegionDetails = regionDetailsList.singleOrNull { it.region.id == "no" } + assertTrue(noRegionDetails != null, "Did not find Norway region") + + assertEquals(Region().id("no").name("Norway"), noRegionDetails.region) + assertEquals(APPROVED, noRegionDetails.status, message = "Wrong State") assertEquals( expected = mapOf( KycType.JUMIO.name to KycStatus.APPROVED), - actual = regionDetails.kycStatusMap) + actual = noRegionDetails.kycStatusMap) } finally { StripePayment.deleteCustomer(customerId = customerId) @@ -977,17 +994,20 @@ class JumioKycTest { body = dataMap } - val regionDetails = get> { + val regionDetailsList = get { path = "/regions" this.email = email - }.single() + } - assertEquals(Region().id("no").name("Norway"), regionDetails.region) - assertEquals(PENDING, regionDetails.status, message = "Wrong State") + val noRegionDetails = regionDetailsList.singleOrNull { it.region.id == "no" } + assertTrue(noRegionDetails != null, "Did not find Norway region") + + assertEquals(Region().id("no").name("Norway"), noRegionDetails.region) + assertEquals(PENDING, noRegionDetails.status, message = "Wrong State") assertEquals( expected = mapOf(KycType.JUMIO.name to KycStatus.REJECTED), - actual = regionDetails.kycStatusMap) + actual = noRegionDetails.kycStatusMap) } finally { StripePayment.deleteCustomer(customerId = customerId) @@ -1026,17 +1046,20 @@ class JumioKycTest { body = dataMap } - val regionDetails = get> { + val regionDetailsList = get { path = "/regions" this.email = email - }.single() + } - assertEquals(Region().id("no").name("Norway"), regionDetails.region) - assertEquals(PENDING, regionDetails.status, message = "Wrong State") + val noRegionDetails = regionDetailsList.singleOrNull { it.region.id == "no" } + assertTrue(noRegionDetails != null, "Did not find Norway region") + + assertEquals(Region().id("no").name("Norway"), noRegionDetails.region) + assertEquals(PENDING, noRegionDetails.status, message = "Wrong State") assertEquals( expected = mapOf(KycType.JUMIO.name to KycStatus.PENDING), - actual = regionDetails.kycStatusMap) + actual = noRegionDetails.kycStatusMap) } finally { StripePayment.deleteCustomer(customerId = customerId) @@ -1075,16 +1098,19 @@ class JumioKycTest { body = dataMap } - val regionDetails = get> { + val regionDetailsList = get { path = "/regions" this.email = email - }.single() + } - assertEquals(Region().id("no").name("Norway"), regionDetails.region) - assertEquals(PENDING, regionDetails.status, message = "Wrong State") + val noRegionDetails = regionDetailsList.singleOrNull { it.region.id == "no" } + assertTrue(noRegionDetails != null, "Did not find Norway region") + + assertEquals(Region().id("no").name("Norway"), noRegionDetails.region) + assertEquals(PENDING, noRegionDetails.status, message = "Wrong State") assertEquals( expected = mapOf(KycType.JUMIO.name to KycStatus.REJECTED), - actual = regionDetails.kycStatusMap) + actual = noRegionDetails.kycStatusMap) } finally { StripePayment.deleteCustomer(customerId = customerId) @@ -1123,16 +1149,19 @@ class JumioKycTest { body = dataMap } - val regionDetails = get> { + val regionDetailsList = get { path = "/regions" this.email = email - }.single() + } + + var noRegionDetails = regionDetailsList.singleOrNull { it.region.id == "no" } + assertTrue(noRegionDetails != null, "Did not find Norway region") - assertEquals(Region().id("no").name("Norway"), regionDetails.region) - assertEquals(PENDING, regionDetails.status, message = "Wrong State") + assertEquals(Region().id("no").name("Norway"), noRegionDetails.region) + assertEquals(PENDING, noRegionDetails.status, message = "Wrong State") assertEquals( expected = mapOf(KycType.JUMIO.name to KycStatus.REJECTED), - actual = regionDetails.kycStatusMap) + actual = noRegionDetails.kycStatusMap) val newScanInfo: ScanInformation = post { path = "/regions/no/kyc/jumio/scans" @@ -1164,17 +1193,20 @@ class JumioKycTest { body = dataMap2 } - val newRegionDetails = get> { + val newRegionDetailsList = get { path = "/regions" this.email = email - }.single() + } + + noRegionDetails = newRegionDetailsList.singleOrNull { it.region.id == "no" } + assertTrue(noRegionDetails != null, "Did not find Norway region") - assertEquals(Region().id("no").name("Norway"), newRegionDetails.region) - assertEquals(APPROVED, newRegionDetails.status, message = "Wrong State") + assertEquals(Region().id("no").name("Norway"), noRegionDetails.region) + assertEquals(APPROVED, noRegionDetails.status, message = "Wrong State") assertEquals( expected = mapOf(KycType.JUMIO.name to KycStatus.APPROVED), - actual = newRegionDetails.kycStatusMap) + actual = noRegionDetails.kycStatusMap) } finally { StripePayment.deleteCustomer(customerId = customerId) @@ -1224,9 +1256,8 @@ class JumioKycTest { } assertEquals("APPROVED", scanInformation.status, message = "Wrong status") - val encodedEmail = URLEncoder.encode(email, StandardCharsets.UTF_8) val scanInformationList = get> { - path = "/support/profiles/$encodedEmail/scans" + path = "/support/profiles/$customerId/scans" this.email = email } assertEquals(1, scanInformationList.size, message = "More scans than expected") @@ -1269,17 +1300,20 @@ class JumioKycTest { body = dataMap } - val newRegionDetails = get> { + val regionDetailsList1 = get { path = "/regions" this.email = email - }.single() + } + + var noRegionDetails = regionDetailsList1.singleOrNull { it.region.id == "no" } + assertTrue(noRegionDetails != null, "Did not find Norway region") - assertEquals(Region().id("no").name("Norway"), newRegionDetails.region) - assertEquals(PENDING, newRegionDetails.status, message = "Wrong State") + assertEquals(Region().id("no").name("Norway"), noRegionDetails.region) + assertEquals(PENDING, noRegionDetails.status, message = "Wrong State") assertEquals( expected = mapOf(KycType.JUMIO.name to KycStatus.REJECTED), - actual = newRegionDetails.kycStatusMap) + actual = noRegionDetails.kycStatusMap) val newScanInfo: ScanInformation = post { path = "/regions/no/kyc/jumio/scans" @@ -1312,21 +1346,23 @@ class JumioKycTest { body = dataMap2 } - val regionDetails = get> { + val regionDetailsList2 = get { path = "/regions" this.email = email - }.single() + } - assertEquals(Region().id("no").name("Norway"), regionDetails.region) - assertEquals(APPROVED, regionDetails.status, message = "Wrong State") + noRegionDetails = regionDetailsList2.singleOrNull { it.region.id == "no" } + assertTrue(noRegionDetails != null, "Did not find Norway region") + + assertEquals(Region().id("no").name("Norway"), noRegionDetails.region) + assertEquals(APPROVED, noRegionDetails.status, message = "Wrong State") assertEquals( expected = mapOf(KycType.JUMIO.name to KycStatus.APPROVED), - actual = regionDetails.kycStatusMap) + actual = noRegionDetails.kycStatusMap) - val encodedEmail = URLEncoder.encode(email, StandardCharsets.UTF_8) val scanInformationList = get> { - path = "/support/profiles/$encodedEmail/scans" + path = "/support/profiles/$customerId/scans" this.email = email } assertEquals(2, scanInformationList.size, message = "More scans than expected") @@ -1374,17 +1410,20 @@ class JumioKycTest { body = dataMap } - val regionDetails = get> { + val regionDetailsList = get { path = "/regions" this.email = email - }.single() + } + val noRegionDetails = regionDetailsList.singleOrNull { it.region.id == "no" } + assertTrue(noRegionDetails != null, "Did not find Norway region") - assertEquals(Region().id("no").name("Norway"), regionDetails.region) - assertEquals(PENDING, regionDetails.status, message = "Wrong State") + + assertEquals(Region().id("no").name("Norway"), noRegionDetails.region) + assertEquals(PENDING, noRegionDetails.status, message = "Wrong State") assertEquals( expected = mapOf(KycType.JUMIO.name to KycStatus.REJECTED), - actual = regionDetails.kycStatusMap) + actual = noRegionDetails.kycStatusMap) } finally { StripePayment.deleteCustomer(customerId = customerId) @@ -1460,8 +1499,10 @@ class SingaporeKycTest { path = "/regions" this.email = email } - - assertTrue(regionDetailsList.isEmpty(), "regionDetailsList should be empty") + regionDetailsList.forEach { + assertTrue(it.status == AVAILABLE, "All regions should be in available state") + assertTrue(it.simProfiles.isEmpty(), "All regions should have empty Sim profile list") + } } val personData: String = get { @@ -1478,7 +1519,8 @@ class SingaporeKycTest { this.email = email } - assertEquals(1, regionDetailsList.size, "regionDetailsList should have only one entry") + val sgRegionDetails = regionDetailsList.singleOrNull { it.region.id == "sg" } + assertTrue(sgRegionDetails != null, "regionDetailsList should contain sg region") val regionDetails = RegionDetails() .region(Region().id("sg").name("Singapore")) @@ -1490,7 +1532,7 @@ class SingaporeKycTest { KycType.NRIC_FIN.name to KycStatus.PENDING)) .simProfiles(SimProfileList()) - assertEquals(regionDetails, regionDetailsList.single(), "RegionDetails do not match") + assertEquals(regionDetails, sgRegionDetails, "RegionDetails do not match") } } finally { StripePayment.deleteCustomer(customerId = customerId) @@ -1512,7 +1554,9 @@ class SingaporeKycTest { this.email = email } - assertTrue(regionDetailsList.isEmpty(), "regionDetailsList should be empty") + regionDetailsList.forEach { + assertTrue(it.status == AVAILABLE, "All regions should be in available state") + } } val personData: String = get { @@ -1529,7 +1573,8 @@ class SingaporeKycTest { this.email = email } - assertEquals(1, regionDetailsList.size, "regionDetailsList should have only one entry") + val newRegionDetailsList = regionDetailsList.singleOrNull { it.region.id == "sg" } + assertTrue(newRegionDetailsList != null, "regionDetailsList should contain sg region") val regionDetails = RegionDetails() .region(Region().id("sg").name("Singapore")) @@ -1541,7 +1586,7 @@ class SingaporeKycTest { KycType.NRIC_FIN.name to KycStatus.PENDING)) .simProfiles(SimProfileList()) - assertEquals(regionDetails, regionDetailsList.single(), "RegionDetails do not match") + assertEquals(regionDetails, newRegionDetailsList, "RegionDetails do not match") } } finally { StripePayment.deleteCustomer(customerId = customerId) @@ -1562,7 +1607,9 @@ class SingaporeKycTest { this.email = email } - assertTrue(regionDetailsList.isEmpty(), "regionDetailsList should be empty") + regionDetailsList.forEach { + assertTrue(it.status == AVAILABLE, "All regions should be in available state") + } } get { @@ -1575,8 +1622,8 @@ class SingaporeKycTest { path = "/regions" this.email = email } - - assertEquals(1, regionDetailsList.size, "regionDetailsList should have only one entry") + val sgRegionDetails = regionDetailsList.singleOrNull { it.region.id == "sg" } + assertTrue(sgRegionDetails != null, "regionDetailsList should contain sg region") val regionDetails = RegionDetails() .region(Region().id("sg").name("Singapore")) @@ -1588,7 +1635,7 @@ class SingaporeKycTest { KycType.ADDRESS.name to KycStatus.PENDING)) .simProfiles(SimProfileList()) - assertEquals(regionDetails, regionDetailsList.single(), "RegionDetails do not match") + assertEquals(regionDetails, sgRegionDetails, "RegionDetails do not match") } val scanInfo: ScanInformation = post { @@ -1621,12 +1668,13 @@ class SingaporeKycTest { } run { - val regionDetailsList = get> { + val regionDetailsList = get { path = "/regions" this.email = email } - assertEquals(1, regionDetailsList.size, "regionDetailsList should have only one entry") + val sgRegionDetails = regionDetailsList.singleOrNull { it.region.id == "sg" } + assertTrue(sgRegionDetails != null, "regionDetailsList should contain sg region") val regionDetails = RegionDetails() .region(Region().id("sg").name("Singapore")) @@ -1638,7 +1686,7 @@ class SingaporeKycTest { KycType.ADDRESS.name to KycStatus.PENDING)) .simProfiles(SimProfileList()) - assertEquals(regionDetails, regionDetailsList.single(), "RegionDetails do not match") + assertEquals(regionDetails, sgRegionDetails, "RegionDetails do not match") } put(expectedResultCode = 204) { @@ -1653,7 +1701,8 @@ class SingaporeKycTest { this.email = email } - assertEquals(1, regionDetailsList.size, "regionDetailsList should have only one entry") + val sgRegionDetails = regionDetailsList.singleOrNull { it.region.id == "sg" } + assertTrue(sgRegionDetails != null, "regionDetailsList should contain sg region") val regionDetails = RegionDetails() .region(Region().id("sg").name("Singapore")) @@ -1665,7 +1714,7 @@ class SingaporeKycTest { KycType.NRIC_FIN.name to KycStatus.APPROVED)) .simProfiles(SimProfileList()) - assertEquals(regionDetails, regionDetailsList.single(), "RegionDetails do not match") + assertEquals(regionDetails, sgRegionDetails, "RegionDetails do not match") } } finally { StripePayment.deleteCustomer(customerId = customerId) @@ -1686,7 +1735,9 @@ class SingaporeKycTest { this.email = email } - assertTrue(regionDetailsList.isEmpty(), "regionDetailsList should be empty") + regionDetailsList.forEach { + assertTrue(it.status == AVAILABLE, "All regions should be in available state") + } } val scanInfo: ScanInformation = post { @@ -1719,12 +1770,13 @@ class SingaporeKycTest { } run { - val regionDetailsList = get> { + val regionDetailsList = get { path = "/regions" this.email = email } - assertEquals(1, regionDetailsList.size, "regionDetailsList should have only one entry") + val sgRegionDetails = regionDetailsList.singleOrNull { it.region.id == "sg" } + assertTrue(sgRegionDetails != null, "regionDetailsList should contain sg region") val regionDetails = RegionDetails() .region(Region().id("sg").name("Singapore")) @@ -1736,7 +1788,7 @@ class SingaporeKycTest { KycType.ADDRESS.name to KycStatus.PENDING)) .simProfiles(SimProfileList()) - assertEquals(regionDetails, regionDetailsList.single(), "RegionDetails do not match") + assertEquals(regionDetails, sgRegionDetails, "RegionDetails do not match") } put(expectedResultCode = 204) { @@ -1751,7 +1803,8 @@ class SingaporeKycTest { this.email = email } - assertEquals(1, regionDetailsList.size, "regionDetailsList should have only one entry") + val sgRegionDetails = regionDetailsList.singleOrNull { it.region.id == "sg" } + assertTrue(sgRegionDetails != null, "regionDetailsList should contain sg region") val regionDetails = RegionDetails() .region(Region().id("sg").name("Singapore")) @@ -1763,7 +1816,7 @@ class SingaporeKycTest { KycType.NRIC_FIN.name to KycStatus.PENDING)) .simProfiles(SimProfileList()) - assertEquals(regionDetails, regionDetailsList.single(), "RegionDetails do not match") + assertEquals(regionDetails, sgRegionDetails, "RegionDetails do not match") } } finally { StripePayment.deleteCustomer(customerId = customerId) diff --git a/acceptance-tests/src/main/kotlin/org/ostelco/at/okhttp/Tests.kt b/acceptance-tests/src/main/kotlin/org/ostelco/at/okhttp/Tests.kt index cf7702fbe..d79a4e7d4 100644 --- a/acceptance-tests/src/main/kotlin/org/ostelco/at/okhttp/Tests.kt +++ b/acceptance-tests/src/main/kotlin/org/ostelco/at/okhttp/Tests.kt @@ -30,6 +30,8 @@ import org.ostelco.prime.customer.model.Region import org.ostelco.prime.customer.model.RegionDetails import org.ostelco.prime.customer.model.RegionDetails.StatusEnum.APPROVED import org.ostelco.prime.customer.model.RegionDetails.StatusEnum.PENDING +import org.ostelco.prime.customer.model.RegionDetails.StatusEnum.AVAILABLE +import org.ostelco.prime.customer.model.RegionDetailsList import org.ostelco.prime.customer.model.ScanInformation import org.ostelco.prime.customer.model.SimProfile import org.ostelco.prime.customer.model.SimProfileList @@ -121,9 +123,12 @@ class RegionsTest { val client = clientForSubject(subject = email) - val regionDetailsList: Collection = client.allRegions + val regionDetailsList: RegionDetailsList = client.allRegions + + regionDetailsList.forEach { + assertTrue(it.status == AVAILABLE, "All regions should be in available state") + } - assertTrue(regionDetailsList.isEmpty(), "RegionDetails list for new customer should be empty") } finally { StripePayment.deleteCustomer(customerId = customerId) } @@ -140,9 +145,10 @@ class RegionsTest { val client = clientForSubject(subject = email) - val regionDetailsList: Collection = client.allRegions + val regionDetailsList: RegionDetailsList = client.allRegions - assertEquals(1, regionDetailsList.size, "Customer should have one region") + val noRegionIndex = regionDetailsList.indexOfFirst { it.region.id == "no" } + assertTrue(noRegionIndex != -1, "regionDetailsList should contain 'no' region") val regionDetails = RegionDetails() .region(Region().id("no").name("Norway")) @@ -150,7 +156,7 @@ class RegionsTest { .kycStatusMap(mapOf(KycType.JUMIO.name to KycStatus.APPROVED)) .simProfiles(SimProfileList()) - assertEquals(regionDetails, regionDetailsList.single(), "RegionDetails do not match") + assertEquals(regionDetails, regionDetailsList[noRegionIndex], "RegionDetails do not match") } finally { StripePayment.deleteCustomer(customerId = customerId) } @@ -663,7 +669,9 @@ class SingaporeKycTest { run { val regionDetailsList = client.allRegions - assertTrue(regionDetailsList.isEmpty(), "regionDetailsList should be empty") + regionDetailsList.forEach { + assertTrue(it.status == AVAILABLE, "All regions should be in available state") + } } val personData: String = jacksonObjectMapper().writeValueAsString(client.getCustomerMyInfoV2Data("authCode")) @@ -674,7 +682,8 @@ class SingaporeKycTest { run { val regionDetailsList = client.allRegions - assertEquals(1, regionDetailsList.size, "regionDetailsList should have only one entry") + val sgRegionIndex = regionDetailsList.indexOfFirst { it.region.id == "sg" } + assertTrue(sgRegionIndex != -1, "regionDetailsList should contain sg region") val regionDetails = RegionDetails() .region(Region().id("sg").name("Singapore")) @@ -686,7 +695,7 @@ class SingaporeKycTest { KycType.NRIC_FIN.name to KycStatus.PENDING)) .simProfiles(SimProfileList()) - assertEquals(regionDetails, regionDetailsList.single(), "RegionDetails do not match") + assertEquals(regionDetails, regionDetailsList[sgRegionIndex], "RegionDetails do not match") } } finally { StripePayment.deleteCustomer(customerId = customerId) @@ -707,7 +716,9 @@ class SingaporeKycTest { run { val regionDetailsList = client.allRegions - assertTrue(regionDetailsList.isEmpty(), "regionDetailsList should be empty") + regionDetailsList.forEach { + assertTrue(it.status == AVAILABLE, "All regions should be in available state") + } } val personData: String = jacksonObjectMapper().writeValueAsString(client.getCustomerMyInfoV3Data("authCode")) @@ -718,7 +729,8 @@ class SingaporeKycTest { run { val regionDetailsList = client.allRegions - assertEquals(1, regionDetailsList.size, "regionDetailsList should have only one entry") + val sgRegionIndex = regionDetailsList.indexOfFirst { it.region.id == "sg" } + assertTrue(sgRegionIndex != -1, "regionDetailsList should contain sg region") val regionDetails = RegionDetails() .region(Region().id("sg").name("Singapore")) @@ -730,7 +742,7 @@ class SingaporeKycTest { KycType.NRIC_FIN.name to KycStatus.PENDING)) .simProfiles(SimProfileList()) - assertEquals(regionDetails, regionDetailsList.single(), "RegionDetails do not match") + assertEquals(regionDetails, regionDetailsList[sgRegionIndex], "RegionDetails do not match") } } finally { StripePayment.deleteCustomer(customerId = customerId) @@ -750,7 +762,9 @@ class SingaporeKycTest { run { val regionDetailsList = client.allRegions - assertTrue(regionDetailsList.isEmpty(), "regionDetailsList should be empty") + regionDetailsList.forEach { + assertTrue(it.status == AVAILABLE, "All regions should be in available state") + } } client.checkNricFinId("S7808018C") @@ -758,7 +772,8 @@ class SingaporeKycTest { run { val regionDetailsList = client.allRegions - assertEquals(1, regionDetailsList.size, "regionDetailsList should have only one entry") + val sgRegionIndex = regionDetailsList.indexOfFirst { it.region.id == "sg" } + assertTrue(sgRegionIndex != -1, "regionDetailsList should contain sg region") val regionDetails = RegionDetails() .region(Region().id("sg").name("Singapore")) @@ -770,7 +785,7 @@ class SingaporeKycTest { KycType.ADDRESS.name to KycStatus.PENDING)) .simProfiles(SimProfileList()) - assertEquals(regionDetails, regionDetailsList.single(), "RegionDetails do not match") + assertEquals(regionDetails, regionDetailsList[sgRegionIndex], "RegionDetails do not match") } val scanInfo: ScanInformation = client.createNewJumioKycScanId("sg") @@ -802,7 +817,8 @@ class SingaporeKycTest { run { val regionDetailsList = client.allRegions - assertEquals(1, regionDetailsList.size, "regionDetailsList should have only one entry") + val sgRegionIndex = regionDetailsList.indexOfFirst { it.region.id == "sg" } + assertTrue(sgRegionIndex != -1, "regionDetailsList should contain sg region") val regionDetails = RegionDetails() .region(Region().id("sg").name("Singapore")) @@ -814,15 +830,16 @@ class SingaporeKycTest { KycType.ADDRESS.name to KycStatus.PENDING)) .simProfiles(SimProfileList()) - assertEquals(regionDetails, regionDetailsList.single(), "RegionDetails do not match") + assertEquals(regionDetails, regionDetailsList[sgRegionIndex], "RegionDetails do not match") } - client.updateDetails("Singapore") + client.updateDetailsForSG("Singapore") run { val regionDetailsList = client.allRegions - assertEquals(1, regionDetailsList.size, "regionDetailsList should have only one entry") + val sgRegionIndex = regionDetailsList.indexOfFirst { it.region.id == "sg" } + assertTrue(sgRegionIndex != -1, "regionDetailsList should contain sg region") val regionDetails = RegionDetails() .region(Region().id("sg").name("Singapore")) @@ -834,7 +851,7 @@ class SingaporeKycTest { KycType.NRIC_FIN.name to KycStatus.APPROVED)) .simProfiles(SimProfileList()) - assertEquals(regionDetails, regionDetailsList.single(), "RegionDetails do not match") + assertEquals(regionDetails, regionDetailsList[sgRegionIndex], "RegionDetails do not match") } } finally { StripePayment.deleteCustomer(customerId = customerId) @@ -854,7 +871,9 @@ class SingaporeKycTest { run { val regionDetailsList = client.allRegions - assertTrue(regionDetailsList.isEmpty(), "regionDetailsList should be empty") + regionDetailsList.forEach { + assertTrue(it.status == AVAILABLE, "All regions should be in available state") + } } val scanInfo: ScanInformation = client.createNewJumioKycScanId("sg") @@ -886,7 +905,8 @@ class SingaporeKycTest { run { val regionDetailsList = client.allRegions - assertEquals(1, regionDetailsList.size, "regionDetailsList should have only one entry") + val sgRegionIndex = regionDetailsList.indexOfFirst { it.region.id == "sg" } + assertTrue(sgRegionIndex != -1, "regionDetailsList should contain sg region") val regionDetails = RegionDetails() .region(Region().id("sg").name("Singapore")) @@ -898,15 +918,16 @@ class SingaporeKycTest { KycType.ADDRESS.name to KycStatus.PENDING)) .simProfiles(SimProfileList()) - assertEquals(regionDetails, regionDetailsList.single(), "RegionDetails do not match") + assertEquals(regionDetails, regionDetailsList[sgRegionIndex], "RegionDetails do not match") } - client.updateDetails("Singapore") + client.updateDetailsForSG("Singapore") run { val regionDetailsList = client.allRegions - assertEquals(1, regionDetailsList.size, "regionDetailsList should have only one entry") + val sgRegionIndex = regionDetailsList.indexOfFirst { it.region.id == "sg" } + assertTrue(sgRegionIndex != -1, "regionDetailsList should contain sg region") val regionDetails = RegionDetails() .region(Region().id("sg").name("Singapore")) @@ -918,7 +939,7 @@ class SingaporeKycTest { KycType.NRIC_FIN.name to KycStatus.PENDING)) .simProfiles(SimProfileList()) - assertEquals(regionDetails, regionDetailsList.single(), "RegionDetails do not match") + assertEquals(regionDetails, regionDetailsList[sgRegionIndex], "RegionDetails do not match") } } finally { StripePayment.deleteCustomer(customerId = customerId) diff --git a/acceptance-tests/src/main/kotlin/org/ostelco/at/pgw/OcsTest.kt b/acceptance-tests/src/main/kotlin/org/ostelco/at/pgw/OcsTest.kt index a8576105b..d68d730f5 100644 --- a/acceptance-tests/src/main/kotlin/org/ostelco/at/pgw/OcsTest.kt +++ b/acceptance-tests/src/main/kotlin/org/ostelco/at/pgw/OcsTest.kt @@ -9,12 +9,14 @@ import org.ostelco.at.common.getLogger import org.ostelco.at.common.randomInt import org.ostelco.at.jersey.get import org.ostelco.diameter.model.FinalUnitAction +import org.ostelco.diameter.model.ReportingReason import org.ostelco.diameter.model.RequestType import org.ostelco.diameter.test.TestClient import org.ostelco.diameter.test.TestHelper import org.ostelco.prime.customer.model.Bundle import java.lang.Thread.sleep import kotlin.test.assertEquals +import kotlin.test.assertNull import kotlin.test.fail /** @@ -78,7 +80,7 @@ class OcsTest { session ) ?: fail("Failed to create request") - TestHelper.createUpdateRequest(request.avps, msisdn, requestedBucketSize, usedBucketSize, ratingGroup, serviceIdentifier) + TestHelper.createUpdateRequest(request.avps, msisdn, requestedBucketSize, usedBucketSize, ratingGroup, serviceIdentifier, ReportingReason.QUOTA_EXHAUSTED) testClient.sendNextRequest(request, session) @@ -109,6 +111,9 @@ class OcsTest { }.first().balance } + /** + * Test that the OCS will correctly handle CCR with Requested-Service-Units for multiple Rating-Groups + */ @Test fun multiRatingGroupsInit() { @@ -356,6 +361,9 @@ class OcsTest { } } + /** + * Test that a users gets correctly denied when the balance on the OCS is used up + */ @Test fun creditControlRequestInitTerminateNoCredit() { @@ -400,14 +408,13 @@ class OcsTest { } // Next request should deny request and grant no quota - val updateRequest = testClient.createRequest( DEST_REALM, DEST_HOST, session ) ?: fail("Failed to create request") - TestHelper.createUpdateRequest(updateRequest.avps, msisdn, BUCKET_SIZE, INITIAL_BALANCE, ratingGroup, serviceIdentifier) + TestHelper.createUpdateRequest(updateRequest.avps, msisdn, BUCKET_SIZE, INITIAL_BALANCE, ratingGroup, serviceIdentifier, ReportingReason.QUOTA_EXHAUSTED) testClient.sendNextRequest(updateRequest, session) @@ -450,7 +457,6 @@ class OcsTest { } // If UE attach again and P-GW tries another CCR-I we should get DIAMETER_CREDIT_LIMIT_REACHED - session = testClient.createSession(object{}.javaClass.enclosingMethod.name + "-2") ?: fail("Failed to create session") request = testClient.createRequest( DEST_REALM, @@ -485,6 +491,10 @@ class OcsTest { } + /** + * Test that the OCS will deny service for users not in the system + */ + @Test fun creditControlRequestInitUnknownUser() { @@ -513,6 +523,10 @@ class OcsTest { } } + /** + * Test CCR with Requested-Service-Units for a Rating-Group only ( no Service-Identifier set ) + */ + @Test fun creditControlRequestInitNoServiceId() { @@ -552,6 +566,352 @@ class OcsTest { } } + + /** + * This test CCR-I without any Requested-Service-Units + */ + + @Test + fun creditControlRequestInitNoRSU() { + + val email = "ocs-${randomInt()}@test.com" + createCustomer(name = "Test OCS User", email = email) + + val msisdn = createSubscription(email = email) + + val session = testClient.createSession(object{}.javaClass.enclosingMethod.name) ?: fail("Failed to create session") + val request = testClient.createRequest( + DEST_REALM, + DEST_HOST, + session + ) ?: fail("Failed to create request") + + TestHelper.createInitRequest(request.avps, msisdn) + + testClient.sendNextRequest(request, session) + + waitForAnswer(session.sessionId) + + run { + val result = testClient.getAnswer(session.sessionId) + assertEquals(DIAMETER_SUCCESS, result?.resultCode) + val resultAvps = result?.resultAvps ?: fail("Missing AVPs") + assertEquals(DEST_HOST, resultAvps.getAvp(Avp.ORIGIN_HOST).utF8String) + assertEquals(DEST_REALM, resultAvps.getAvp(Avp.ORIGIN_REALM).utF8String) + assertEquals(RequestType.INITIAL_REQUEST.toLong(), resultAvps.getAvp(Avp.CC_REQUEST_TYPE).integer32.toLong()) + val resultMSCC = resultAvps.getAvp(Avp.MULTIPLE_SERVICES_CREDIT_CONTROL) + assertNull(resultMSCC, "There should not be any MSCC if there is no MSCC in the CCR") + } + } + + /** + * This test CCR-I without any Requested-Service-Units + */ + + @Test + fun creditControlRequestInitNoRsuUnknownUser() { + + val session = testClient.createSession(object{}.javaClass.enclosingMethod.name) ?: fail("Failed to create session") + val request = testClient.createRequest( + DEST_REALM, + DEST_HOST, + session + ) ?: fail("Failed to create request") + + TestHelper.createInitRequest(request.avps, "1337") + + testClient.sendNextRequest(request, session) + + waitForAnswer(session.sessionId) + + run { + val result = testClient.getAnswer(session.sessionId) + assertEquals(DIAMETER_USER_UNKNOWN, result?.resultCode) + val resultAvps = result?.resultAvps ?: fail("Missing AVPs") + assertEquals(DEST_HOST, resultAvps.getAvp(Avp.ORIGIN_HOST).utF8String) + assertEquals(DEST_REALM, resultAvps.getAvp(Avp.ORIGIN_REALM).utF8String) + assertEquals(RequestType.INITIAL_REQUEST.toLong(), resultAvps.getAvp(Avp.CC_REQUEST_TYPE).integer32.toLong()) + val resultMSCC = resultAvps.getAvp(Avp.MULTIPLE_SERVICES_CREDIT_CONTROL) + assertNull(resultMSCC, "There should not be any MSCC if there is no MSCC in the CCR") + } + } + + + /** + * Test with: + * CCR-I that has no MSCC or Requested-Service-Units. + * CCR-U with MSCC and Requested-Service-Units. + * The user should not have any balance, so we should se DIAMETER_CREDIT_LIMIT_REACHED + */ + @Test + fun creditControlRequestInitNoRsuUpdateWithRsuNoBalance() { + + val email = "ocs-${randomInt()}@test.com" + createCustomer(name = "Test OCS User", email = email) + + val msisdn = createSubscription(email = email) + + val session = testClient.createSession(object{}.javaClass.enclosingMethod.name) ?: fail("Failed to create session") + val initRequest = testClient.createRequest( + DEST_REALM, + DEST_HOST, + session + ) ?: fail("Failed to create request") + + val ratingGroup = 10 + val serviceIdentifier = -1 + + // Authenticate + TestHelper.createInitRequest(initRequest.avps, msisdn) + + testClient.sendNextRequest(initRequest, session) + + waitForAnswer(session.sessionId) + + run { + val result = testClient.getAnswer(session.sessionId) + assertEquals(DIAMETER_SUCCESS, result?.resultCode) + val resultAvps = result?.resultAvps ?: fail("Missing AVPs") + assertEquals(DEST_HOST, resultAvps.getAvp(Avp.ORIGIN_HOST).utF8String) + assertEquals(DEST_REALM, resultAvps.getAvp(Avp.ORIGIN_REALM).utF8String) + assertEquals(RequestType.INITIAL_REQUEST.toLong(), resultAvps.getAvp(Avp.CC_REQUEST_TYPE).integer32.toLong()) + val resultMSCC = resultAvps.getAvp(Avp.MULTIPLE_SERVICES_CREDIT_CONTROL) + assertNull(resultMSCC, "There should not be any MSCC if there is no MSCC in the CCR") + } + + + // Use up all quota + val updateRequest = testClient.createRequest( + DEST_REALM, + DEST_HOST, + session + ) ?: fail("Failed to create request") + + TestHelper.createUpdateRequest(updateRequest.avps, msisdn, INITIAL_BALANCE + BUCKET_SIZE, 0L, ratingGroup, serviceIdentifier, ReportingReason.QUOTA_EXHAUSTED) + + testClient.sendNextRequest(updateRequest, session) + + waitForAnswer(session.sessionId) + + // First bucket request should reserve at least the default bucket balance + run { + val result = testClient.getAnswer(session.sessionId) + assertEquals(DIAMETER_SUCCESS, result?.resultCode) + val resultAvps = result?.resultAvps ?: fail("Missing AVPs") + assertEquals(DEST_HOST, resultAvps.getAvp(Avp.ORIGIN_HOST).utF8String) + assertEquals(DEST_REALM, resultAvps.getAvp(Avp.ORIGIN_REALM).utF8String) + assertEquals(RequestType.UPDATE_REQUEST.toLong(), resultAvps.getAvp(Avp.CC_REQUEST_TYPE).integer32.toLong()) + val resultMSCC = resultAvps.getAvp(Avp.MULTIPLE_SERVICES_CREDIT_CONTROL) + assertEquals(DIAMETER_SUCCESS, resultMSCC.grouped.getAvp(Avp.RESULT_CODE).integer32.toLong()) + assertEquals(ratingGroup.toLong(), resultMSCC.grouped.getAvp(Avp.RATING_GROUP).integer32.toLong()) + val granted = resultMSCC.grouped.getAvp(Avp.GRANTED_SERVICE_UNIT) + assertEquals(INITIAL_BALANCE, granted.grouped.getAvp(Avp.CC_TOTAL_OCTETS).unsigned64) + } + + val updateRequest2 = testClient.createRequest( + DEST_REALM, + DEST_HOST, + session + ) ?: fail("Failed to create request") + + TestHelper.createUpdateRequest(updateRequest2.avps, msisdn, INITIAL_BALANCE, INITIAL_BALANCE, ratingGroup, serviceIdentifier, ReportingReason.QUOTA_EXHAUSTED) + + testClient.sendNextRequest(updateRequest2, session) + + waitForAnswer(session.sessionId) + + run { + val result = testClient.getAnswer(session.sessionId) + assertEquals(DIAMETER_SUCCESS, result?.resultCode) + val resultAvps = result?.resultAvps ?: fail("Missing AVPs") + assertEquals(DEST_HOST, resultAvps.getAvp(Avp.ORIGIN_HOST).utF8String) + assertEquals(DEST_REALM, resultAvps.getAvp(Avp.ORIGIN_REALM).utF8String) + assertEquals(RequestType.UPDATE_REQUEST.toLong(), resultAvps.getAvp(Avp.CC_REQUEST_TYPE).integer32.toLong()) + val resultMSCC = resultAvps.getAvp(Avp.MULTIPLE_SERVICES_CREDIT_CONTROL) + assertEquals(ratingGroup.toLong(), resultMSCC.grouped.getAvp(Avp.RATING_GROUP).integer32.toLong()) + assertEquals(DIAMETER_CREDIT_LIMIT_REACHED, resultMSCC.grouped.getAvp(Avp.RESULT_CODE).integer32.toLong()) + } + + // Simulate UE disconnect by P-GW sending CCR-Terminate + val terminateRequest = testClient.createRequest( + DEST_REALM, + DEST_HOST, + session + ) ?: fail("Failed to create request") + TestHelper.createTerminateRequest(terminateRequest.avps, msisdn) + + testClient.sendNextRequest(terminateRequest, session) + + waitForAnswer(session.sessionId) + + run { + val result = testClient.getAnswer(session.sessionId) + assertEquals(DIAMETER_SUCCESS, result?.resultCode) + val resultAvps = result?.resultAvps ?: fail("Missing AVPs") + assertEquals(DEST_HOST, resultAvps.getAvp(Avp.ORIGIN_HOST).utF8String) + assertEquals(DEST_REALM, resultAvps.getAvp(Avp.ORIGIN_REALM).utF8String) + assertEquals(RequestType.TERMINATION_REQUEST.toLong(), resultAvps.getAvp(Avp.CC_REQUEST_TYPE).integer32.toLong()) + } + + + // Now restart again to get denied on first bucket request + val session2 = testClient.createSession(object{}.javaClass.enclosingMethod.name) ?: fail("Failed to create session") + + val initRequest2 = testClient.createRequest( + DEST_REALM, + DEST_HOST, + session2 + ) ?: fail("Failed to create request") + + + TestHelper.createInitRequest(initRequest2.avps, msisdn) + + testClient.sendNextRequest(initRequest2, session2) + + waitForAnswer(session2.sessionId) + + run { + val result = testClient.getAnswer(session2.sessionId) + assertEquals(DIAMETER_SUCCESS, result?.resultCode) + val resultAvps = result?.resultAvps ?: fail("Missing AVPs") + assertEquals(DEST_HOST, resultAvps.getAvp(Avp.ORIGIN_HOST).utF8String) + assertEquals(DEST_REALM, resultAvps.getAvp(Avp.ORIGIN_REALM).utF8String) + assertEquals(RequestType.INITIAL_REQUEST.toLong(), resultAvps.getAvp(Avp.CC_REQUEST_TYPE).integer32.toLong()) + val resultMSCC = resultAvps.getAvp(Avp.MULTIPLE_SERVICES_CREDIT_CONTROL) + assertNull(resultMSCC, "There should not be any MSCC if there is no MSCC in the CCR") + } + + // First Update Request with Requested-Service-Units ( no Used-Service-Units ), this should now be denied + val updateRequest3 = testClient.createRequest( + DEST_REALM, + DEST_HOST, + session2 + ) + + TestHelper.createUpdateRequest(updateRequest3!!.avps, msisdn, 0L, -1L, ratingGroup, serviceIdentifier, ReportingReason.QUOTA_EXHAUSTED) + + testClient.sendNextRequest(updateRequest3, session2) + + waitForAnswer(session2.sessionId) + + run { + val result = testClient.getAnswer(session2.sessionId) + assertEquals(DIAMETER_SUCCESS, result?.resultCode) + val resultAvps = result?.resultAvps ?: fail("Missing AVPs") + assertEquals(DEST_HOST, resultAvps.getAvp(Avp.ORIGIN_HOST).utF8String) + assertEquals(DEST_REALM, resultAvps.getAvp(Avp.ORIGIN_REALM).utF8String) + assertEquals(RequestType.UPDATE_REQUEST.toLong(), resultAvps.getAvp(Avp.CC_REQUEST_TYPE).integer32.toLong()) + val resultMSCC = resultAvps.getAvp(Avp.MULTIPLE_SERVICES_CREDIT_CONTROL) + assertEquals(DIAMETER_CREDIT_LIMIT_REACHED, resultMSCC.grouped.getAvp(Avp.RESULT_CODE).integer32.toLong()) + assertEquals(ratingGroup.toLong(), resultMSCC.grouped.getAvp(Avp.RATING_GROUP).integer32.toLong()) + val granted = resultMSCC.grouped.getAvp(Avp.GRANTED_SERVICE_UNIT) + assertEquals(0L, granted.grouped.getAvp(Avp.CC_TOTAL_OCTETS).unsigned64) + } + } + + /** + * This test will check that we can handle CCR-U requests that also report CC-Time and CC-Service-Specific-Units + * in separate Used-Service-Units in the MSCC + */ + + @Test + fun creditControlRequestInitUpdateCCTime() { + val email = "ocs-${randomInt()}@test.com" + createCustomer(name = "Test OCS User", email = email) + + val msisdn = createSubscription(email = email) + + val ratingGroup = 10 + val serviceIdentifier = -1 + + val session = testClient.createSession(object{}.javaClass.enclosingMethod.name) ?: fail("Failed to create session") + val initRequest = testClient.createRequest( + DEST_REALM, + DEST_HOST, + session + ) ?: fail("Failed to create request") + + // CCR-I is without any Requested-Service-Unints + TestHelper.createInitRequest(initRequest.avps, msisdn) + + testClient.sendNextRequest(initRequest, session) + + waitForAnswer(session.sessionId) + + run { + val result = testClient.getAnswer(session.sessionId) + assertEquals(DIAMETER_SUCCESS, result?.resultCode) + val resultAvps = result?.resultAvps ?: fail("Missing AVPs") + assertEquals(DEST_HOST, resultAvps.getAvp(Avp.ORIGIN_HOST).utF8String) + assertEquals(DEST_REALM, resultAvps.getAvp(Avp.ORIGIN_REALM).utF8String) + assertEquals(RequestType.INITIAL_REQUEST.toLong(), resultAvps.getAvp(Avp.CC_REQUEST_TYPE).integer32.toLong()) + val resultMSCC = resultAvps.getAvp(Avp.MULTIPLE_SERVICES_CREDIT_CONTROL) + assertNull(resultMSCC, "There should not be any MSCC if there is no MSCC in the CCR") + } + + + // First Update Request with Requested-Service-Units ( no Used-Service-Units ) + val updateRequest1 = testClient.createRequest( + DEST_REALM, + DEST_HOST, + session + ) + + TestHelper.createUpdateRequest(updateRequest1!!.avps, msisdn, 0L, 0L, ratingGroup, serviceIdentifier, 725L, 1L) + + testClient.sendNextRequest(updateRequest1, session) + + waitForAnswer(session.sessionId) + + run { + val result = testClient.getAnswer(session.sessionId) + assertEquals(DIAMETER_SUCCESS, result?.resultCode) + val resultAvps = result?.resultAvps ?: fail("Missing AVPs") + assertEquals(DEST_HOST, resultAvps.getAvp(Avp.ORIGIN_HOST).utF8String) + assertEquals(DEST_REALM, resultAvps.getAvp(Avp.ORIGIN_REALM).utF8String) + assertEquals(RequestType.UPDATE_REQUEST.toLong(), resultAvps.getAvp(Avp.CC_REQUEST_TYPE).integer32.toLong()) + val resultMSCC = resultAvps.getAvp(Avp.MULTIPLE_SERVICES_CREDIT_CONTROL) + assertEquals(DIAMETER_SUCCESS, resultMSCC.grouped.getAvp(Avp.RESULT_CODE).integer32.toLong()) + assertEquals(ratingGroup.toLong(), resultMSCC.grouped.getAvp(Avp.RATING_GROUP).integer32.toLong()) + val granted = resultMSCC.grouped.getAvp(Avp.GRANTED_SERVICE_UNIT) + assertEquals(DEFAULT_REQUESTED_SERVICE_UNIT, granted.grouped.getAvp(Avp.CC_TOTAL_OCTETS).unsigned64) + } + + + // Second Update Request with Requested-Service-Units and Used-Service-Units + val updateRequest2 = testClient.createRequest( + DEST_REALM, + DEST_HOST, + session + ) + + TestHelper.createUpdateRequest(updateRequest2!!.avps, msisdn, 0L, DEFAULT_REQUESTED_SERVICE_UNIT, ratingGroup, serviceIdentifier, 725L, 1L) + + testClient.sendNextRequest(updateRequest2, session) + + waitForAnswer(session.sessionId) + + run { + val result = testClient.getAnswer(session.sessionId) + assertEquals(DIAMETER_SUCCESS, result?.resultCode) + val resultAvps = result?.resultAvps ?: fail("Missing AVPs") + assertEquals(DEST_HOST, resultAvps.getAvp(Avp.ORIGIN_HOST).utF8String) + assertEquals(DEST_REALM, resultAvps.getAvp(Avp.ORIGIN_REALM).utF8String) + assertEquals(RequestType.UPDATE_REQUEST.toLong(), resultAvps.getAvp(Avp.CC_REQUEST_TYPE).integer32.toLong()) + val resultMSCC = resultAvps.getAvp(Avp.MULTIPLE_SERVICES_CREDIT_CONTROL) + assertEquals(DIAMETER_SUCCESS, resultMSCC.grouped.getAvp(Avp.RESULT_CODE).integer32.toLong()) + assertEquals(ratingGroup.toLong(), resultMSCC.grouped.getAvp(Avp.RATING_GROUP).integer32.toLong()) + val granted = resultMSCC.grouped.getAvp(Avp.GRANTED_SERVICE_UNIT) + assertEquals(DEFAULT_REQUESTED_SERVICE_UNIT, granted.grouped.getAvp(Avp.CC_TOTAL_OCTETS).unsigned64) + } + + } + + /** + * Test that the default bucket size is used by the OCS when the CCR only contain + * Requested-Service-Unit without specified value. + */ + @Test fun creditControlRequestInitUpdateAndTerminateNoRequestedServiceUnit() { @@ -588,6 +948,9 @@ class OcsTest { } + /** + * Test that the OCS will handle CCR-U that does not contain any Requested-Service-Units only Used-Service-Units + */ @Test fun simpleCreditControlRequestInitUpdateNoRSU() { @@ -611,7 +974,7 @@ class OcsTest { session ) - TestHelper.createUpdateRequest(request!!.avps, msisdn, -1L, DEFAULT_REQUESTED_SERVICE_UNIT, ratingGroup, serviceIdentifier) + TestHelper.createUpdateRequest(request!!.avps, msisdn, -1L, DEFAULT_REQUESTED_SERVICE_UNIT, ratingGroup, serviceIdentifier, ReportingReason.QUOTA_EXHAUSTED) testClient.sendNextRequest(request, session) @@ -625,6 +988,51 @@ class OcsTest { } + /** + * Test that CCR-U with Reporting-Reason QHT ( no new Requested-Service-Unit ) works. + */ + + @Test + fun testNoMsccInCcrU() { + + val email = "ocs-${randomInt()}@test.com" + createCustomer(name = "Test OCS User", email = email) + + val msisdn = createSubscription(email = email) + + val ratingGroup = 10 + val serviceIdentifier = -1 + + val session = testClient.createSession(object{}.javaClass.enclosingMethod.name) ?: fail("Failed to create session") + + // This test assume that the default bucket size is set to 4000000L + simpleCreditControlRequestInit(session, msisdn,0L, DEFAULT_REQUESTED_SERVICE_UNIT, ratingGroup, serviceIdentifier) + + val updateRequest = testClient.createRequest( + DEST_REALM, + DEST_HOST, + session + ) + + TestHelper.createUpdateRequest(updateRequest!!.getAvps(), msisdn, -1L, 500_000L, ratingGroup, serviceIdentifier, ReportingReason.QHT) + + testClient.sendNextRequest(updateRequest, session) + + waitForAnswer(session.getSessionId()) + + val result = testClient.getAnswer(session.getSessionId()) + assertEquals(DIAMETER_SUCCESS, result!!.resultCode!!.toLong()) + val resultAvps = result.resultAvps + assertEquals(DEST_HOST, resultAvps.getAvp(Avp.ORIGIN_HOST).getUTF8String()) + assertEquals(DEST_REALM, resultAvps.getAvp(Avp.ORIGIN_REALM).getUTF8String()) + assertEquals(RequestType.UPDATE_REQUEST.toLong(), resultAvps.getAvp(Avp.CC_REQUEST_TYPE).getInteger32().toLong()) + val resultMSCC = resultAvps.getAvp(Avp.MULTIPLE_SERVICES_CREDIT_CONTROL) + assertNull(resultMSCC, "No requested MSCC") + assertEquals(86400, resultAvps.getAvp(Avp.VALIDITY_TIME).getInteger32().toLong()) + + } + + // pubsub answer can take up to 10 seconds on the emulator private fun waitForAnswer(sessionId: String) { diff --git a/admin-endpoint/build.gradle.kts b/admin-endpoint/build.gradle.kts index 4a13c6d5d..e16ad95e0 100644 --- a/admin-endpoint/build.gradle.kts +++ b/admin-endpoint/build.gradle.kts @@ -12,4 +12,4 @@ dependencies { testImplementation("io.dropwizard:dropwizard-testing:${Version.dropwizard}") } -apply(from = "../gradle/jacoco.gradle") \ No newline at end of file +apply(from = "../gradle/jacoco.gradle.kts") \ No newline at end of file diff --git a/analytics-module/build.gradle.kts b/analytics-module/build.gradle.kts index 2f2faedb7..88e4e612c 100644 --- a/analytics-module/build.gradle.kts +++ b/analytics-module/build.gradle.kts @@ -9,5 +9,5 @@ dependencies { implementation(project(":prime-modules")) implementation("com.google.cloud:google-cloud-pubsub:${Version.googleCloudPubSub}") - implementation("com.google.code.gson:gson:2.8.5") + implementation("com.google.code.gson:gson:${Version.gson}") } diff --git a/analytics-module/src/main/kotlin/org/ostelco/prime/analytics/publishers/DelegatePubSubPublisher.kt b/analytics-module/src/main/kotlin/org/ostelco/prime/analytics/publishers/DelegatePubSubPublisher.kt index 599ba22ff..255e5cbac 100644 --- a/analytics-module/src/main/kotlin/org/ostelco/prime/analytics/publishers/DelegatePubSubPublisher.kt +++ b/analytics-module/src/main/kotlin/org/ostelco/prime/analytics/publishers/DelegatePubSubPublisher.kt @@ -59,15 +59,18 @@ class DelegatePubSubPublisher( override fun onFailure(throwable: Throwable) { if (throwable is ApiException) { // details on the API exception - logger.warn("Status code: {}", throwable.statusCode.code) - logger.warn("Retrying: {}", throwable.isRetryable) + logger.warn("Error publishing message to Pubsub topic: $topicId\n" + + "Message: ${throwable.message}\n" + + "Status code: ${throwable.statusCode.code}\n" + + "Retrying: ${throwable.isRetryable}") + } else { + logger.warn("Error publishing message to Pubsub topic: $topicId") } - logger.warn("Error publishing message in topic: $topicId") } override fun onSuccess(messageId: String) { // Once published, returns server-assigned message ids (unique within the topic) - logger.debug("Published message $messageId") + logger.debug("Published message $messageId to topic $topicId") } }, DataConsumptionInfoPublisher.singleThreadScheduledExecutor) } diff --git a/app-notifier/src/main/kotlin/org/ostelco/prime/appnotifier/FirebaseAppNotifier.kt b/app-notifier/src/main/kotlin/org/ostelco/prime/appnotifier/FirebaseAppNotifier.kt index ef766fe4a..4d11c43bb 100644 --- a/app-notifier/src/main/kotlin/org/ostelco/prime/appnotifier/FirebaseAppNotifier.kt +++ b/app-notifier/src/main/kotlin/org/ostelco/prime/appnotifier/FirebaseAppNotifier.kt @@ -9,76 +9,106 @@ import com.google.firebase.messaging.FirebaseMessagingException import com.google.firebase.messaging.Message import com.google.firebase.messaging.Notification import org.ostelco.prime.getLogger +import org.ostelco.prime.model.ApplicationToken +import org.ostelco.prime.model.FCMStrings import org.ostelco.prime.module.getResource import org.ostelco.prime.storage.ClientDataSource class FirebaseAppNotifier: AppNotifier { private val logger by getLogger() - val listOfFailureCodes = listOf( + /* Ref. to Firebase. */ + private val store = getResource() + + /* Firebase messaging failure cases. */ + private val listOfFailureCodes = listOf( "messaging/invalid-recipient", "messaging/invalid-registration-token", - "messaging/registration-token-not-registered" + "messaging/registration-token-not-registered", + "registration-token-not-registered" ) - override fun notify(customerId: String, title: String, body: String) { - logger.info("Will try to notify customer with Id : $customerId") - sendNotification(customerId, title, body, data = null) - } + override fun notify(notificationType: NotificationType, customerId: String, data: Map) = + when (notificationType) { + NotificationType.JUMIO_VERIFICATION_SUCCEEDED -> { + logger.info("Notifying customer $customerId of successful JUMIO verification") + sendMessage(customerId = customerId, + title = FCMStrings.JUMIO_NOTIFICATION_TITLE.s, + body = FCMStrings.JUMIO_IDENTITY_VERIFIED.s, + data = data) + } + NotificationType.JUMIO_VERIFICATION_FAILED -> { + logger.info("Notifying customer $customerId of failed JUMIO verification " + + "with data $data") + sendMessage(customerId = customerId, + title = FCMStrings.JUMIO_NOTIFICATION_TITLE.s, + body = FCMStrings.JUMIO_IDENTITY_FAILED.s, + data = data) + } + } - override fun notify(customerId: String, title: String, body: String, data: Map) { - logger.info("Will try to notify-with-data customer with Id : $customerId $body, $data") - sendNotification(customerId, title, body, data) + override fun notify(customerId: String, title: String, body: String, data: Map) { + logger.info("Notifying customer $customerId of message $body with data $data") + sendMessage(customerId, title, body, data) } - private fun sendNotification(customerId: String, title: String, body: String, data: Map?) { - - val store = getResource() - - // This registration token comes from the client FCM SDKs. - val applicationTokens = store.getNotificationTokens(customerId) - - for (applicationToken in applicationTokens) { + private fun sendMessage(customerId: String, title: String, body: String, data: Map) = + store.getNotificationTokens(customerId) + .filter { + it.tokenType == "FCM" + } + .forEach { + sendMessage( + customerId = customerId, + token = it, + message = Message.builder() + .setNotification( + Notification(title, body)) + .setToken(it.token) + .putAllData(data.mapValues { + it.value.toString() + }) + .build() + ) + } - if (applicationToken.tokenType == "FCM") { - // See documentation on defining a message payload. - val builder = Message.builder() - .setNotification(Notification(title, body)) - .setToken(applicationToken.token) - if (data != null) { - builder.putAllData(data) + /* Send a message asynchrounously to the device corresponding to + the provided registration token. */ + private fun sendMessage(customerId: String, + token: ApplicationToken, + message: Message) { + val future = FirebaseMessaging + .getInstance(FirebaseApp.getInstance("fcm")) + .sendAsync(message) + val apiFutureCallback = object : ApiFutureCallback { + override fun onSuccess(result: String) { + logger.info("Notification for $customerId with appId: ${token.applicationID} " + + "completed with result: $result") + if (listOfFailureCodes.contains(result)) { + store.removeNotificationToken(customerId, token.applicationID) } - val message = builder.build() - - // Send a message to the device corresponding to the provided - // registration token. - val future = FirebaseMessaging - .getInstance(FirebaseApp.getInstance("fcm")) - .sendAsync(message) - - val apiFutureCallback = object : ApiFutureCallback { - override fun onSuccess(result: String) { - logger.info("Notification for $customerId with appId: ${applicationToken.applicationID} completed with result: $result") - if (listOfFailureCodes.contains(result)) { - store.removeNotificationToken(customerId, applicationToken.applicationID) - } - } + } - override fun onFailure(t: Throwable) { - if (t is FirebaseMessagingException) { - val errorCode = t.errorCode - logger.warn("Notification for $customerId with appId: ${applicationToken.applicationID} failed with errorCode: $errorCode") - if (listOfFailureCodes.contains(errorCode)) { - logger.warn("Removing failed token for $customerId with appId: ${applicationToken.applicationID} token: $applicationToken.token") - store.removeNotificationToken(customerId, applicationToken.applicationID) - } - } else { - logger.warn("Notification for $customerId with appId: ${applicationToken.applicationID} failed with error: $t") - } + override fun onFailure(t: Throwable) { + if (t is FirebaseMessagingException) { + val errorCode = t.errorCode + if (listOfFailureCodes.contains(errorCode)) { + // Known failure, we should remove this token from our list + logger.info("Removing failed token (errorCode: $errorCode) for $customerId with appId: ${token.applicationID} " + + "token: $token.token") + store.removeNotificationToken(customerId, token.applicationID) + } else { + // Other failures we should look into. + logger.warn("Notification for $customerId with appId: ${token.applicationID} " + + "failed with errorCode: $errorCode") } + } else { + logger.warn("Notification for $customerId with appId: ${token.applicationID} " + + "failed with error: $t") } - addCallback(future, apiFutureCallback, directExecutor()) } } + + addCallback(future, apiFutureCallback, directExecutor()) } } \ No newline at end of file diff --git a/appleid-auth-service/build.gradle.kts b/appleid-auth-service/build.gradle.kts index 768d49edc..f077d3af7 100644 --- a/appleid-auth-service/build.gradle.kts +++ b/appleid-auth-service/build.gradle.kts @@ -31,4 +31,4 @@ tasks.withType { archiveVersion.set("") } -apply(from = "../gradle/jacoco.gradle") +apply(from = "../gradle/jacoco.gradle.kts") diff --git a/auth-server/Dockerfile b/auth-server/Dockerfile index d9dbdd240..76cbe7cb6 100644 --- a/auth-server/Dockerfile +++ b/auth-server/Dockerfile @@ -1,4 +1,4 @@ -FROM azul/zulu-openjdk:12.0.2 +FROM azul/zulu-openjdk:13 LABEL maintainer="dev@redotter.sg" diff --git a/auth-server/build.gradle.kts b/auth-server/build.gradle.kts index 618331bf8..fc02349fb 100644 --- a/auth-server/build.gradle.kts +++ b/auth-server/build.gradle.kts @@ -59,7 +59,7 @@ val integration = tasks.create("integration", Test::class.java) { tasks.build.get().dependsOn(integration) integration.mustRunAfter(tasks.test) -apply(from = "../gradle/jacoco.gradle") +apply(from = "../gradle/jacoco.gradle.kts") idea { module { diff --git a/build.gradle.kts b/build.gradle.kts index 600455f60..81c6b2e13 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -8,7 +8,7 @@ plugins { base java id("project-report") - id("com.github.ben-manes.versions") version "0.25.0" + id("com.github.ben-manes.versions") version "0.27.0" jacoco kotlin("jvm") version "1.3.50" apply false id("com.google.protobuf") version "0.8.10" apply false @@ -33,7 +33,7 @@ allprojects { } jacoco { - toolVersion = "0.8.2" + toolVersion = "0.8.4" } } diff --git a/buildSrc/src/main/kotlin/org/ostelco/prime/gradle/Version.kt b/buildSrc/src/main/kotlin/org/ostelco/prime/gradle/Version.kt index ce2eeb67b..b9c27fdef 100644 --- a/buildSrc/src/main/kotlin/org/ostelco/prime/gradle/Version.kt +++ b/buildSrc/src/main/kotlin/org/ostelco/prime/gradle/Version.kt @@ -4,26 +4,26 @@ object Version { const val assertJ = "3.13.2" const val arrow = "0.8.2" - const val arrowTypeClasses = "0.9.0" - const val beam = "2.15.0" + const val byteBuddy = "1.10.2" const val csv = "1.7" const val cxf = "3.3.3" const val dockerComposeJunitRule = "1.3.0" - const val dropwizard = "1.3.14" - const val metrics = "4.1.0" + const val dropwizard = "1.3.15" + const val metrics = "4.1.1" const val firebase = "6.10.0" - const val googleCloud = "1.91.0" - const val googleCloudDataStore = "1.92.0" - const val googleCloudLogging = "0.110.0-alpha" - const val googleCloudPubSub = "1.92.0" - const val googleCloudStorage = "1.92.0" + const val googleCloud = "1.91.2" + const val googleCloudDataStore = "1.98.0" + const val googleCloudLogging = "0.116.0-alpha" + const val googleCloudPubSub = "1.98.0" + const val googleCloudStorage = "1.98.0" - const val grpc = "1.23.0" + const val gson = "2.8.6" + const val grpc = "1.24.0" const val guava = "28.1-jre" - const val jackson = "2.9.9" - const val jacksonDatabind = "2.9.9.3" + const val jackson = "2.10.0" + const val jacksonDatabind = "2.10.0" const val javaxActivation = "1.1.1" const val javaxActivationApi = "1.2.0" const val javaxAnnotation = "1.3.2" @@ -34,11 +34,11 @@ object Version { const val jjwt = "0.10.7" const val junit5 = "5.5.2" const val kotlin = "1.3.50" - const val kotlinXCoroutines = "1.3.1" - const val mockito = "3.0.0" + const val kotlinXCoroutines = "1.3.2" + const val mockito = "3.1.0" const val mockitoKotlin = "2.2.0" const val neo4jDriver = "1.7.5" - const val neo4j = "3.5.9" + const val neo4j = "3.5.12" const val opencensus = "0.24.0" const val postgresql = "42.2.8" // See comment in ./sim-administration/simmanager/build.gradle const val prometheusDropwizard = "2.2.0" @@ -46,10 +46,10 @@ object Version { const val slf4j = "1.7.28" // IMPORTANT: When Stripe SDK library version is updated, check if the Stripe API version has changed. // If so, then update API version in Stripe Web Console for callback Webhooks. - const val stripe = "12.0.0" - const val swagger = "2.0.9" - const val swaggerCodegen = "2.4.8" - const val testcontainers = "1.12.1" + const val stripe = "14.0.1" + const val swagger = "2.0.10" + const val swaggerCodegen = "2.4.9" + const val testcontainers = "1.12.2" const val tink = "1.2.2" const val zxing = "3.4.0" } \ No newline at end of file diff --git a/customer-endpoint/build.gradle.kts b/customer-endpoint/build.gradle.kts index e67e06a93..7b71ca916 100644 --- a/customer-endpoint/build.gradle.kts +++ b/customer-endpoint/build.gradle.kts @@ -16,7 +16,13 @@ dependencies { testRuntimeOnly("io.jsonwebtoken:jjwt-impl:${Version.jjwt}") testRuntimeOnly("io.jsonwebtoken:jjwt-jackson:${Version.jjwt}") - testImplementation("com.nhaarman:mockito-kotlin:1.6.0") + testImplementation("com.nhaarman.mockitokotlin2:mockito-kotlin:${Version.mockitoKotlin}") + testImplementation("net.bytebuddy:byte-buddy:${Version.byteBuddy}") { + because("mockito-kotlin:2.2.0 has byte-buddy:1.9.0 which does not work for java13") + } + testImplementation("net.bytebuddy:byte-buddy-agent:${Version.byteBuddy}") { + because("mockito-kotlin:2.2.0 has byte-buddy:1.9.0 which does not work for java13") + } } -apply(from = "../gradle/jacoco.gradle") \ No newline at end of file +apply(from = "../gradle/jacoco.gradle.kts") \ No newline at end of file diff --git a/customer-endpoint/src/main/kotlin/org/ostelco/prime/customer/endpoint/resources/KycResources.kt b/customer-endpoint/src/main/kotlin/org/ostelco/prime/customer/endpoint/resources/KycResources.kt index 63023fad7..a97959a39 100644 --- a/customer-endpoint/src/main/kotlin/org/ostelco/prime/customer/endpoint/resources/KycResources.kt +++ b/customer-endpoint/src/main/kotlin/org/ostelco/prime/customer/endpoint/resources/KycResources.kt @@ -104,22 +104,19 @@ class SingaporeKycResource(private val dao: SubscriberDAO): KycResource(regionCo }.build() } - @EnableTracing - @PUT @Path("/profile") - @Produces(MediaType.APPLICATION_JSON) - fun saveProfile(@Auth token: AccessTokenPrincipal?, - @NotNull - @QueryParam("address") - address: String): Response = - if (token == null) { - Response.status(Response.Status.UNAUTHORIZED) - } else { - dao.saveAddress( - identity = token.identity, - address = address) - .responseBuilder(Response.Status.NO_CONTENT) - }.build() + fun saveProfile(): ProfileKycResource = ProfileKycResource(regionCode = "sg", dao = dao) +} + +/** + * [MalaysiaKycResource] uses [JumioKycResource] via parent class [KycResource]. + * It has Malaysia specific eKYC APIs. + * + */ +class MalaysiaKycResource(private val dao: SubscriberDAO): KycResource(regionCode = "my", dao = dao) { + + @Path("/profile") + fun saveProfile(): ProfileKycResource = ProfileKycResource(regionCode = "my", dao = dao) } class MyInfoResource(private val dao: SubscriberDAO, @@ -155,6 +152,26 @@ class MyInfoResource(private val dao: SubscriberDAO, }.build() } +class ProfileKycResource(private val regionCode: String, private val dao: SubscriberDAO) { + + @EnableTracing + @PUT + @Produces(MediaType.APPLICATION_JSON) + fun saveProfile(@Auth token: AccessTokenPrincipal?, + @NotNull + @QueryParam("address") + address: String): Response = + if (token == null) { + Response.status(Response.Status.UNAUTHORIZED) + } else { + dao.saveAddress( + identity = token.identity, + address = address, + regionCode = regionCode) + .responseBuilder(Response.Status.NO_CONTENT) + }.build() +} + class JumioKycResource(private val regionCode: String, private val dao: SubscriberDAO) { @EnableTracing diff --git a/customer-endpoint/src/main/kotlin/org/ostelco/prime/customer/endpoint/resources/RegionsResource.kt b/customer-endpoint/src/main/kotlin/org/ostelco/prime/customer/endpoint/resources/RegionsResource.kt index 55deb96f2..704c59979 100644 --- a/customer-endpoint/src/main/kotlin/org/ostelco/prime/customer/endpoint/resources/RegionsResource.kt +++ b/customer-endpoint/src/main/kotlin/org/ostelco/prime/customer/endpoint/resources/RegionsResource.kt @@ -48,6 +48,7 @@ class RegionsResource(private val dao: SubscriberDAO) { regionCode: String ): KycResource = when (regionCode.toLowerCase()) { "sg" -> SingaporeKycResource(dao = dao) + "my" -> MalaysiaKycResource(dao = dao) else -> KycResource(regionCode = regionCode, dao = dao) } diff --git a/customer-endpoint/src/main/kotlin/org/ostelco/prime/customer/endpoint/store/SubscriberDAO.kt b/customer-endpoint/src/main/kotlin/org/ostelco/prime/customer/endpoint/store/SubscriberDAO.kt index 11ed81684..cf21c5b94 100644 --- a/customer-endpoint/src/main/kotlin/org/ostelco/prime/customer/endpoint/store/SubscriberDAO.kt +++ b/customer-endpoint/src/main/kotlin/org/ostelco/prime/customer/endpoint/store/SubscriberDAO.kt @@ -120,7 +120,7 @@ interface SubscriberDAO { fun checkNricFinIdUsingDave(identity: Identity, nricFinId: String): Either - fun saveAddress(identity: Identity, address: String): Either + fun saveAddress(identity: Identity, address: String, regionCode: String): Either // // Token diff --git a/customer-endpoint/src/main/kotlin/org/ostelco/prime/customer/endpoint/store/SubscriberDAOImpl.kt b/customer-endpoint/src/main/kotlin/org/ostelco/prime/customer/endpoint/store/SubscriberDAOImpl.kt index 0ef574f92..91b70d672 100644 --- a/customer-endpoint/src/main/kotlin/org/ostelco/prime/customer/endpoint/store/SubscriberDAOImpl.kt +++ b/customer-endpoint/src/main/kotlin/org/ostelco/prime/customer/endpoint/store/SubscriberDAOImpl.kt @@ -458,8 +458,8 @@ class SubscriberDAOImpl : SubscriberDAO { .mapLeft { mapStorageErrorToApiError("Invalid NRIC/FIN ID", ApiErrorCode.INVALID_NRIC_FIN_ID, it) } } - override fun saveAddress(identity: Identity, address: String): Either { - return storage.saveAddress(identity = identity, address = address) + override fun saveAddress(identity: Identity, address: String, regionCode: String): Either { + return storage.saveAddress(identity = identity, address = address, regionCode = regionCode) .mapLeft { mapStorageErrorToApiError("Failed to save address", ApiErrorCode.FAILED_TO_SAVE_ADDRESS, it) } } diff --git a/customer-endpoint/src/test/kotlin/org/ostelco/prime/customer/endpoint/resources/ApplicationTokenResourceTest.kt b/customer-endpoint/src/test/kotlin/org/ostelco/prime/customer/endpoint/resources/ApplicationTokenResourceTest.kt index 8b95593f5..51bf53d94 100644 --- a/customer-endpoint/src/test/kotlin/org/ostelco/prime/customer/endpoint/resources/ApplicationTokenResourceTest.kt +++ b/customer-endpoint/src/test/kotlin/org/ostelco/prime/customer/endpoint/resources/ApplicationTokenResourceTest.kt @@ -2,7 +2,7 @@ package org.ostelco.prime.customer.endpoint.resources import arrow.core.Either import arrow.core.right -import com.nhaarman.mockito_kotlin.argumentCaptor +import com.nhaarman.mockitokotlin2.argumentCaptor import io.dropwizard.auth.AuthDynamicFeature import io.dropwizard.auth.AuthValueFactoryProvider import io.dropwizard.auth.oauth.OAuthCredentialAuthFilter diff --git a/customer-endpoint/src/test/kotlin/org/ostelco/prime/customer/endpoint/resources/CustomerResourceTest.kt b/customer-endpoint/src/test/kotlin/org/ostelco/prime/customer/endpoint/resources/CustomerResourceTest.kt index 5f4e7c831..789a40142 100644 --- a/customer-endpoint/src/test/kotlin/org/ostelco/prime/customer/endpoint/resources/CustomerResourceTest.kt +++ b/customer-endpoint/src/test/kotlin/org/ostelco/prime/customer/endpoint/resources/CustomerResourceTest.kt @@ -1,7 +1,7 @@ package org.ostelco.prime.customer.endpoint.resources import arrow.core.Either -import com.nhaarman.mockito_kotlin.argumentCaptor +import com.nhaarman.mockitokotlin2.argumentCaptor import io.dropwizard.auth.AuthDynamicFeature import io.dropwizard.auth.AuthValueFactoryProvider import io.dropwizard.auth.oauth.OAuthCredentialAuthFilter diff --git a/customer-endpoint/src/test/kotlin/org/ostelco/prime/customer/endpoint/resources/KycResourcesTest.kt b/customer-endpoint/src/test/kotlin/org/ostelco/prime/customer/endpoint/resources/KycResourcesTest.kt index 75b4109c5..5a914deb5 100644 --- a/customer-endpoint/src/test/kotlin/org/ostelco/prime/customer/endpoint/resources/KycResourcesTest.kt +++ b/customer-endpoint/src/test/kotlin/org/ostelco/prime/customer/endpoint/resources/KycResourcesTest.kt @@ -2,8 +2,8 @@ package org.ostelco.prime.customer.endpoint.resources import arrow.core.Either import arrow.core.right -import com.nhaarman.mockito_kotlin.argumentCaptor -import com.nhaarman.mockito_kotlin.eq +import com.nhaarman.mockitokotlin2.argumentCaptor +import com.nhaarman.mockitokotlin2.eq import io.dropwizard.auth.AuthDynamicFeature import io.dropwizard.auth.AuthValueFactoryProvider import io.dropwizard.auth.oauth.OAuthCredentialAuthFilter.Builder diff --git a/customer-endpoint/src/test/kotlin/org/ostelco/prime/customer/endpoint/resources/ProductsResourceTest.kt b/customer-endpoint/src/test/kotlin/org/ostelco/prime/customer/endpoint/resources/ProductsResourceTest.kt index 754505b74..6db9d6115 100644 --- a/customer-endpoint/src/test/kotlin/org/ostelco/prime/customer/endpoint/resources/ProductsResourceTest.kt +++ b/customer-endpoint/src/test/kotlin/org/ostelco/prime/customer/endpoint/resources/ProductsResourceTest.kt @@ -1,7 +1,7 @@ package org.ostelco.prime.customer.endpoint.resources import arrow.core.Either -import com.nhaarman.mockito_kotlin.argumentCaptor +import com.nhaarman.mockitokotlin2.argumentCaptor import io.dropwizard.auth.AuthDynamicFeature import io.dropwizard.auth.AuthValueFactoryProvider import io.dropwizard.auth.oauth.OAuthCredentialAuthFilter diff --git a/customer-endpoint/src/test/kotlin/org/ostelco/prime/customer/endpoint/resources/PurchasesResourceTest.kt b/customer-endpoint/src/test/kotlin/org/ostelco/prime/customer/endpoint/resources/PurchasesResourceTest.kt index 9f246f928..eaedfcaf4 100644 --- a/customer-endpoint/src/test/kotlin/org/ostelco/prime/customer/endpoint/resources/PurchasesResourceTest.kt +++ b/customer-endpoint/src/test/kotlin/org/ostelco/prime/customer/endpoint/resources/PurchasesResourceTest.kt @@ -1,7 +1,7 @@ package org.ostelco.prime.customer.endpoint.resources import arrow.core.Either -import com.nhaarman.mockito_kotlin.argumentCaptor +import com.nhaarman.mockitokotlin2.argumentCaptor import io.dropwizard.auth.AuthDynamicFeature import io.dropwizard.auth.AuthValueFactoryProvider import io.dropwizard.auth.oauth.OAuthCredentialAuthFilter.Builder diff --git a/customer-endpoint/src/test/kotlin/org/ostelco/prime/customer/endpoint/resources/RegionsResourceTest.kt b/customer-endpoint/src/test/kotlin/org/ostelco/prime/customer/endpoint/resources/RegionsResourceTest.kt index 92d12da27..e7a4dc787 100644 --- a/customer-endpoint/src/test/kotlin/org/ostelco/prime/customer/endpoint/resources/RegionsResourceTest.kt +++ b/customer-endpoint/src/test/kotlin/org/ostelco/prime/customer/endpoint/resources/RegionsResourceTest.kt @@ -2,7 +2,7 @@ package org.ostelco.prime.customer.endpoint.resources import arrow.core.Either import arrow.core.right -import com.nhaarman.mockito_kotlin.argumentCaptor +import com.nhaarman.mockitokotlin2.argumentCaptor import io.dropwizard.auth.AuthDynamicFeature import io.dropwizard.auth.AuthValueFactoryProvider import io.dropwizard.auth.oauth.OAuthCredentialAuthFilter.Builder diff --git a/customer-endpoint/src/test/kotlin/org/ostelco/prime/customer/endpoint/resources/SubscriptionsResourceTest.kt b/customer-endpoint/src/test/kotlin/org/ostelco/prime/customer/endpoint/resources/SubscriptionsResourceTest.kt index 8622a3581..a40ebc9b6 100644 --- a/customer-endpoint/src/test/kotlin/org/ostelco/prime/customer/endpoint/resources/SubscriptionsResourceTest.kt +++ b/customer-endpoint/src/test/kotlin/org/ostelco/prime/customer/endpoint/resources/SubscriptionsResourceTest.kt @@ -1,7 +1,7 @@ package org.ostelco.prime.customer.endpoint.resources import arrow.core.Either -import com.nhaarman.mockito_kotlin.argumentCaptor +import com.nhaarman.mockitokotlin2.argumentCaptor import io.dropwizard.auth.AuthDynamicFeature import io.dropwizard.auth.AuthValueFactoryProvider import io.dropwizard.auth.oauth.OAuthCredentialAuthFilter diff --git a/customer-support-endpoint/build.gradle.kts b/customer-support-endpoint/build.gradle.kts index 4a13c6d5d..e16ad95e0 100644 --- a/customer-support-endpoint/build.gradle.kts +++ b/customer-support-endpoint/build.gradle.kts @@ -12,4 +12,4 @@ dependencies { testImplementation("io.dropwizard:dropwizard-testing:${Version.dropwizard}") } -apply(from = "../gradle/jacoco.gradle") \ No newline at end of file +apply(from = "../gradle/jacoco.gradle.kts") \ No newline at end of file diff --git a/customer-support-endpoint/src/main/kotlin/org/ostelco/prime/support/resources/HoustonResources.kt b/customer-support-endpoint/src/main/kotlin/org/ostelco/prime/support/resources/HoustonResources.kt index c17c53490..ba4e2d87f 100644 --- a/customer-support-endpoint/src/main/kotlin/org/ostelco/prime/support/resources/HoustonResources.kt +++ b/customer-support-endpoint/src/main/kotlin/org/ostelco/prime/support/resources/HoustonResources.kt @@ -3,6 +3,7 @@ package org.ostelco.prime.support.resources import arrow.core.Either import arrow.core.flatMap import arrow.core.left +import arrow.core.right import io.dropwizard.auth.Auth import org.ostelco.prime.apierror.ApiError import org.ostelco.prime.apierror.ApiErrorCode @@ -52,22 +53,25 @@ class ProfilesResource { * Get the subscriber profile. */ @GET - @Path("{id}") + @Path("{query}") @Produces(MediaType.APPLICATION_JSON) - fun getProfile(@Auth token: AccessTokenPrincipal?, - @NotNull - @PathParam("id") - id: String): Response = + fun getCustomerList(@Auth token: AccessTokenPrincipal?, + @NotNull + @PathParam("query") + query: String): Response = if (token == null) { Response.status(Response.Status.UNAUTHORIZED) } else { - if (!isEmail(id)) { - logger.info("${token.name} Accessing profile for msisdn:$id") - getProfileForMsisdn(id) + val trimmedQuery = query.trim() + .replace("'", "") + .replace("\"", "") + if (isMsisdn(trimmedQuery)) { + logger.info("${token.name} Accessing profile for msisdn: $query") + getProfileListForMsisdn(trimmedQuery) .responseBuilder() } else { - logger.info("${token.name} Accessing profile for email:$id") - getProfile(contactEmail = id) + logger.info("${token.name} Accessing profile for nickname/contactEmail: $query") + getCustomerList(queryString = trimmedQuery) .responseBuilder() } }.build() @@ -76,17 +80,17 @@ class ProfilesResource { * Get the subscriptions for this subscriber. */ @GET - @Path("{email}/subscriptions") + @Path("{id}/subscriptions") @Produces(MediaType.APPLICATION_JSON) fun getSubscriptions(@Auth token: AccessTokenPrincipal?, @NotNull - @PathParam("email") - email: String): Response = + @PathParam("id") + id: String): Response = if (token == null) { Response.status(Response.Status.UNAUTHORIZED) } else { - logger.info("${token.name} Accessing subscriptions for email:$email") - getSubscriptions(contactEmail = email) + logger.info("${token.name} Accessing subscriptions for customerId: $id") + getSubscriptions(customerId = id) .responseBuilder() }.build() @@ -95,43 +99,51 @@ class ProfilesResource { * Get all the eKYC scan information for this subscriber. */ @GET - @Path("{email}/scans") + @Path("{id}/scans") @Produces(MediaType.APPLICATION_JSON) fun getAllScanInformation(@Auth token: AccessTokenPrincipal?, @NotNull - @PathParam("email") - email: String): Response = + @PathParam("id") + id: String): Response = if (token == null) { Response.status(Response.Status.UNAUTHORIZED) } else { - logger.info("${token.name} Accessing scan information for email:$email") - getAllScanInformation(contactEmail = email) + logger.info("${token.name} Accessing scan information for customerId: $id") + getAllScanInformation(customerId = id) .responseBuilder() }.build() - private fun getAllScanInformation(contactEmail: String): Either> { + private fun getAllScanInformation(customerId: String): Either> { return try { - storage.getIdentityForContactEmail(contactEmail = contactEmail).flatMap {identity: Identity -> + storage.getIdentityForCustomerId(id = customerId).flatMap { identity: Identity -> storage.getAllScanInformation(identity = identity) }.mapLeft { NotFoundError("Failed to fetch scan information.", ApiErrorCode.FAILED_TO_FETCH_SCAN_INFORMATION, it) } } catch (e: Exception) { - logger.error("Failed to fetch scan information for customer with contactEmail - $contactEmail", e) + logger.error("Failed to fetch scan information for customer with customerId: $customerId", e) Either.left(NotFoundError("Failed to fetch scan information", ApiErrorCode.FAILED_TO_FETCH_SCAN_INFORMATION)) } } // TODO: Reuse the one from SubscriberDAO - private fun getProfile(contactEmail: String): Either { + private fun getCustomerList(queryString: String): Either> { return try { - storage.getIdentityForContactEmail(contactEmail = contactEmail).flatMap {identity: Identity -> - storage.getCustomer(identity) - }.mapLeft { + storage.getIdentitiesFor(queryString = queryString).mapLeft { NotFoundError("Failed to fetch profile.", ApiErrorCode.FAILED_TO_FETCH_CUSTOMER, it) + }.flatMap { identityList -> + val customerList = identityList.mapNotNull { identity: Identity -> + storage.getCustomer(identity).fold( + { + logger.error("Error fetching customer for $queryString, $it") + null + }, + { it }) + }.distinctBy { it.id } + Either.right(customerList) } } catch (e: Exception) { - logger.error("Failed to fetch profile for customer with contactEmail - $contactEmail", e) + logger.error("Failed to fetch profile for customer with nickname/contactEmail - $queryString", e) Either.left(NotFoundError("Failed to fetch profile", ApiErrorCode.FAILED_TO_FETCH_CUSTOMER)) } } @@ -142,10 +154,18 @@ class ProfilesResource { return pattern.matcher(email).matches() } - private fun getProfileForMsisdn(msisdn: String): Either { + private fun isMsisdn(query: String): Boolean { + val regex = "^[0-9]+$" + val pattern = Pattern.compile(regex) + return pattern.matcher(query).matches() + } + + private fun getProfileListForMsisdn(msisdn: String): Either> { return try { storage.getCustomerForMsisdn(msisdn).mapLeft { NotFoundError("Failed to fetch profile.", ApiErrorCode.FAILED_TO_FETCH_CUSTOMER, it) + }.map { + listOf(it) } } catch (e: Exception) { logger.error("Failed to fetch profile for msisdn $msisdn", e) @@ -154,78 +174,18 @@ class ProfilesResource { } // TODO: Reuse the one from SubscriberDAO - private fun getSubscriptions(contactEmail: String): Either> { + private fun getSubscriptions(customerId: String): Either> { return try { - storage.getIdentityForContactEmail(contactEmail = contactEmail).flatMap {identity: Identity -> + storage.getIdentityForCustomerId(id = customerId).flatMap { identity: Identity -> storage.getSubscriptions(identity) }.mapLeft { NotFoundError("Failed to get subscriptions.", ApiErrorCode.FAILED_TO_FETCH_SUBSCRIPTIONS, it) } } catch (e: Exception) { - logger.error("Failed to get subscriptions for customer with contactEmail - $contactEmail", e) + logger.error("Failed to get subscriptions for customer with customerId: - $customerId", e) InternalServerError("Failed to get subscriptions", ApiErrorCode.FAILED_TO_FETCH_SUBSCRIPTIONS).left() } } - - /** - * Fetches and return all plans that a subscriber subscribes - * to if any. - */ - @GET - @Path("{email}/plans") - @Produces("application/json") - fun getPlans(@PathParam("email") email: String): Response = - storage.getIdentityForContactEmail(contactEmail = email).flatMap { identity: Identity -> - storage.getPlans(identity = identity) - }.mapLeft { - ApiErrorMapper.mapStorageErrorToApiError("Failed to fetch plans", - ApiErrorCode.FAILED_TO_FETCH_PLANS_FOR_SUBSCRIBER, - it) - } - .responseBuilder() - .build() - - /** - * Attaches (subscribes) a subscriber to a plan. - */ - @POST - @Path("{email}/plans/{planId}") - @Produces("application/json") - fun attachPlan(@PathParam("email") email: String, - @PathParam("planId") planId: String, - @QueryParam("trial_end") trialEnd: Long): Response = - storage.getIdentityForContactEmail(contactEmail = email).flatMap { identity: Identity -> - storage.subscribeToPlan( - identity = identity, - planId = planId, - trialEnd = trialEnd) - }.mapLeft { - ApiErrorMapper.mapStorageErrorToApiError("Failed to store subscription", - ApiErrorCode.FAILED_TO_STORE_SUBSCRIPTION, - it) - } - .responseBuilder(Response.Status.CREATED) - .build() - - /** - * Removes a plan from the list subscriptions for a subscriber. - */ - @DELETE - @Path("{email}/plans/{planId}") - @Produces("application/json") - fun detachPlan(@PathParam("email") email: String, - @PathParam("planId") planId: String): Response = - storage.getIdentityForContactEmail(contactEmail = email).flatMap { identity: Identity -> - storage.unsubscribeFromPlan( - identity = identity, - planId = planId) - }.mapLeft { - ApiErrorMapper.mapStorageErrorToApiError("Failed to remove subscription", - ApiErrorCode.FAILED_TO_REMOVE_SUBSCRIPTION, - it) - } - .responseBuilder() - .build() } /** @@ -240,30 +200,29 @@ class BundlesResource { * Get all bundles for the subscriber. */ @GET - @Path("{email}") + @Path("{id}") @Produces(MediaType.APPLICATION_JSON) - fun getBundlesByEmail(@Auth token: AccessTokenPrincipal?, - @NotNull - @PathParam("email") - email: String): Response = + fun getBundlesById(@Auth token: AccessTokenPrincipal?, + @NotNull + @PathParam("id") + id: String): Response = if (token == null) { Response.status(Response.Status.UNAUTHORIZED) } else { - logger.info("${token.name} Accessing bundles for $email") - getBundles(contactEmail = email) - .responseBuilder() + logger.info("${token.name} Accessing bundles for customerId: $id") + getBundles(customerId = id).responseBuilder() }.build() // TODO: Reuse the one from SubscriberDAO - private fun getBundles(contactEmail: String): Either> { + private fun getBundles(customerId: String): Either> { return try { - storage.getIdentityForContactEmail(contactEmail = contactEmail).flatMap { identity: Identity -> + storage.getIdentityForCustomerId(id = customerId).flatMap { identity: Identity -> storage.getBundles(identity) }.mapLeft { NotFoundError("Failed to get bundles. ${it.message}", ApiErrorCode.FAILED_TO_FETCH_BUNDLES) } } catch (e: Exception) { - logger.error("Failed to get bundles for customer with contactEmail - $contactEmail", e) + logger.error("Failed to get bundles for customer with customerId: $customerId", e) Either.left(NotFoundError("Failed to get bundles", ApiErrorCode.FAILED_TO_FETCH_BUNDLES)) } } @@ -281,30 +240,29 @@ class PurchaseResource { * Get all purchase history for the subscriber. */ @GET - @Path("{email}") + @Path("{id}") @Produces(MediaType.APPLICATION_JSON) - fun getPurchaseHistoryByEmail(@Auth token: AccessTokenPrincipal?, - @NotNull - @PathParam("email") - email: String): Response = + fun getPurchaseHistoryById(@Auth token: AccessTokenPrincipal?, + @NotNull + @PathParam("id") + id: String): Response = if (token == null) { Response.status(Response.Status.UNAUTHORIZED) } else { - logger.info("${token.name} Accessing bundles for $email") - getPurchaseHistory(contactEmail = email) - .responseBuilder() + logger.info("${token.name} Accessing bundles for customerId: $id") + getPurchaseHistory(customerId = id).responseBuilder() }.build() // TODO: Reuse the one from SubscriberDAO - private fun getPurchaseHistory(contactEmail: String): Either> { + private fun getPurchaseHistory(customerId: String): Either> { return try { - storage.getIdentityForContactEmail(contactEmail = contactEmail).flatMap { identity: Identity -> + storage.getIdentityForCustomerId(id = customerId).flatMap { identity: Identity -> storage.getPurchaseRecords(identity) }.bimap( { NotFoundError("Failed to get purchase history.", ApiErrorCode.FAILED_TO_FETCH_PAYMENT_HISTORY, it) }, { it.toList() }) } catch (e: Exception) { - logger.error("Failed to get purchase history for customer with contactEmail - $contactEmail", e) + logger.error("Failed to get purchase history for customer with customerId - $customerId", e) Either.left(InternalServerError("Failed to get purchase history", ApiErrorCode.FAILED_TO_FETCH_PAYMENT_HISTORY)) } } @@ -322,33 +280,33 @@ class RefundResource { * Refund a specified purchase for the subscriber. */ @PUT - @Path("{email}") + @Path("{id}") @Produces(MediaType.APPLICATION_JSON) - fun refundPurchaseByEmail(@Auth token: AccessTokenPrincipal?, - @NotNull - @PathParam("email") - email: String, - @NotNull - @QueryParam("purchaseRecordId") - purchaseRecordId: String, - @NotNull - @QueryParam("reason") - reason: String): Response = + fun refundPurchaseById(@Auth token: AccessTokenPrincipal?, + @NotNull + @PathParam("id") + id: String, + @NotNull + @QueryParam("purchaseRecordId") + purchaseRecordId: String, + @NotNull + @QueryParam("reason") + reason: String): Response = if (token == null) { Response.status(Response.Status.UNAUTHORIZED) } else { - logger.info("${token.name} Refunding purchase for $email at id: $purchaseRecordId") - refundPurchase(email, purchaseRecordId, reason) + logger.info("${token.name} Refunding purchase for customerId: $id at id: $purchaseRecordId") + refundPurchase(id, purchaseRecordId, reason) .map { - logger.info(NOTIFY_OPS_MARKER, "${token.name} refunded the purchase (id:$purchaseRecordId) for $email ") + logger.info(NOTIFY_OPS_MARKER, "${token.name} refunded the purchase (id:$purchaseRecordId) for customerId: $id ") it } .responseBuilder() }.build() - private fun refundPurchase(contactEmail: String, purchaseRecordId: String, reason: String): Either { + private fun refundPurchase(customerId: String, purchaseRecordId: String, reason: String): Either { return try { - storage.getIdentityForContactEmail(contactEmail = contactEmail).flatMap { identity: Identity -> + storage.getIdentityForCustomerId(id = customerId).flatMap { identity: Identity -> storage.refundPurchase(identity, purchaseRecordId, reason) }.mapLeft { when (it) { @@ -357,7 +315,7 @@ class RefundResource { } } } catch (e: Exception) { - logger.error("Failed to refund purchase for customer with contactEmail - $contactEmail, id: $purchaseRecordId", e) + logger.error("Failed to refund purchase for customer with customerId - $customerId, id: $purchaseRecordId", e) Either.left(InternalServerError("Failed to refund purchase", ApiErrorCode.FAILED_TO_REFUND_PURCHASE)) } } @@ -375,24 +333,24 @@ class ContextResource { * Get context for the subscriber. */ @GET - @Path("{email}") + @Path("{id}") @Produces(MediaType.APPLICATION_JSON) - fun getContextByEmail(@Auth token: AccessTokenPrincipal?, - @NotNull - @PathParam("email") - email: String): Response = + fun getContextById(@Auth token: AccessTokenPrincipal?, + @NotNull + @PathParam("id") + id: String): Response = if (token == null) { Response.status(Response.Status.UNAUTHORIZED) } else { - logger.info("${token.name} Accessing context for $email") - getContext(contactEmail = email) + logger.info("${token.name} Accessing context for customer with id: $id") + getContext(customerId = id) .responseBuilder() }.build() // TODO: Reuse the one from SubscriberDAO - fun getContext(contactEmail: String): Either { + private fun getContext(customerId: String): Either { return try { - storage.getIdentityForContactEmail(contactEmail = contactEmail).flatMap { identity: Identity -> + storage.getIdentityForCustomerId(id = customerId).flatMap { identity: Identity -> storage.getCustomer(identity).map { customer -> storage.getAllRegionDetails(identity = identity) .fold( @@ -403,71 +361,43 @@ class ContextResource { NotFoundError("Failed to fetch customer.", ApiErrorCode.FAILED_TO_FETCH_CONTEXT, it) } } catch (e: Exception) { - logger.error("Failed to fetch context for customer with contactEmail - $contactEmail", e) + logger.error("Failed to fetch context for customer with id - $customerId", e) Either.left(NotFoundError("Failed to fetch context", ApiErrorCode.FAILED_TO_FETCH_CONTEXT)) } } } -open class HoustonResource { - private val logger by getLogger() - - private val storage by lazy { getResource() } - private val notifier by lazy { getResource() } - - fun getCustomerId(contactEmail: String): Either { - return try { - storage.getIdentityForContactEmail(contactEmail = contactEmail).flatMap { identity: Identity -> - storage.getCustomer(identity = identity) - }.mapLeft { - NotFoundError("Did not find subscription.", ApiErrorCode.FAILED_TO_FETCH_SUBSCRIPTIONS, it) - }.map { - it.id - } - } catch (e: Exception) { - logger.error("Did not find subscription for email $contactEmail", e) - InternalServerError("Did not find subscription", ApiErrorCode.FAILED_TO_FETCH_SUBSCRIPTIONS).left() - } - } -} - /** * Resource used to handle notification related REST calls. */ @Path("/support/notify") -class NotifyResource: HoustonResource() { +class NotifyResource { private val logger by getLogger() - - private val storage by lazy { getResource() } private val notifier by lazy { getResource() } /** * Sends a notification to all devices for a subscriber. */ @PUT - @Path("{email}") + @Path("{id}") @Produces(MediaType.APPLICATION_JSON) - fun sendNotificationByEmail(@Auth token: AccessTokenPrincipal?, - @NotNull - @PathParam("email") - email: String, - @NotNull - @QueryParam("title") - title: String, - @NotNull - @QueryParam("message") - message: String): Response = + fun sendNotificationById(@Auth token: AccessTokenPrincipal?, + @NotNull + @PathParam("id") + id: String, + @NotNull + @QueryParam("title") + title: String, + @NotNull + @QueryParam("message") + message: String): Response = if (token == null) { Response.status(Response.Status.UNAUTHORIZED) } else { - getCustomerId(contactEmail = email) - .map { customerId -> - logger.info("${token.name} Sending notification to $email customerId: $customerId") - val data = mapOf("timestamp" to "${System.currentTimeMillis()}") - notifier.notify(customerId, title, message, data) - customerId - } - .responseBuilder("Message Sent") + logger.info("${token.name} Sending notification to customerId: $id") + val data = mapOf("timestamp" to "${System.currentTimeMillis()}") + notifier.notify(id, title, message, data) + id.right().responseBuilder("Message Sent") }.build() } @@ -476,35 +406,28 @@ class NotifyResource: HoustonResource() { * Resource used to handle audit log related REST calls. */ @Path("/support/auditLog") -class AuditLogResource: HoustonResource() { - +class AuditLogResource { private val logger by getLogger() - - private val storage by lazy { getResource() } private val auditLogStore by lazy { getResource() } /** * Fetch all audit logs for a subscriber. */ @GET - @Path("{email}") + @Path("{id}") @Produces(MediaType.APPLICATION_JSON) - fun query(@Auth token: AccessTokenPrincipal?, - @NotNull - @PathParam("email") - email: String): Response = + fun queryAuditLogs(@Auth token: AccessTokenPrincipal?, + @NotNull + @PathParam("id") + id: String): Response = if (token == null) { Response.status(Response.Status.UNAUTHORIZED) } else { - getCustomerId(contactEmail = email) - .flatMap { customerId -> - logger.info("${token.name} fetching audit log of $email customerId: $customerId") - auditLogStore.getCustomerActivityHistory(customerId = customerId) - .mapLeft { errorMessage -> - InternalServerError(errorMessage, FAILED_TO_FETCH_AUDIT_LOGS) - } - } - .responseBuilder() + logger.info("${token.name} fetching audit log of customerId: $id") + auditLogStore.getCustomerActivityHistory(customerId = id) + .mapLeft { errorMessage -> + InternalServerError(errorMessage, FAILED_TO_FETCH_AUDIT_LOGS) + }.responseBuilder() }.build() } @@ -513,7 +436,6 @@ class AuditLogResource: HoustonResource() { */ @Path("/support/customer") class CustomerResource { - private val logger by getLogger() private val storage by lazy { getResource() } @@ -521,30 +443,29 @@ class CustomerResource { * Remove customer from Prime. */ @DELETE - @Path("{email}") + @Path("{id}") @Produces(MediaType.APPLICATION_JSON) fun removeCustomer(@Auth token: AccessTokenPrincipal?, - @NotNull - @PathParam("email") - email: String): Response = + @NotNull + @PathParam("id") + id: String): Response = if (token == null) { Response.status(Response.Status.UNAUTHORIZED) } else { - logger.info("${token.name} Removing the customer for $email") - removeCustomer(contactEmail = email) - .responseBuilder(Response.Status.NO_CONTENT) + logger.info("${token.name} Removing the customer for customerId: $id") + removeCustomer(customerId = id).responseBuilder(Response.Status.NO_CONTENT) }.build() // TODO: Reuse the one from SubscriberDAO - private fun removeCustomer(contactEmail: String): Either { + private fun removeCustomer(customerId: String): Either { return try { - storage.getIdentityForContactEmail(contactEmail = contactEmail).flatMap { identity: Identity -> + storage.getIdentityForCustomerId(id = customerId).flatMap { identity: Identity -> storage.removeCustomer(identity) }.mapLeft { NotFoundError("Failed to remove customer.", ApiErrorCode.FAILED_TO_REMOVE_CUSTOMER, it) } } catch (e: Exception) { - logger.error("Failed to fetch profile for customer with contactEmail - $contactEmail", e) + logger.error("Failed to fetch profile for customer with customerId - $customerId", e) Either.left(NotFoundError("Failed to remove customer", ApiErrorCode.FAILED_TO_REMOVE_CUSTOMER)) } } diff --git a/data-store/build.gradle.kts b/data-store/build.gradle.kts index 6635574e0..cb3dc5694 100644 --- a/data-store/build.gradle.kts +++ b/data-store/build.gradle.kts @@ -22,4 +22,4 @@ dependencies { testImplementation("org.mockito:mockito-core:${Version.mockito}") } -apply(from = "../gradle/jacoco.gradle") \ No newline at end of file +apply(from = "../gradle/jacoco.gradle.kts") \ No newline at end of file diff --git a/data-store/src/main/kotlin/org/ostelco/prime/store/datastore/Schema.kt b/data-store/src/main/kotlin/org/ostelco/prime/store/datastore/Schema.kt index d10d0b0bf..7167025f1 100644 --- a/data-store/src/main/kotlin/org/ostelco/prime/store/datastore/Schema.kt +++ b/data-store/src/main/kotlin/org/ostelco/prime/store/datastore/Schema.kt @@ -129,7 +129,7 @@ class EntityStore( // convert object to map of (field name, field value) // TODO: Fails to serialize datastore 'Value<*>' types such as 'StringValue'. - val map: MutableMap = objectMapper.convertValue(entity, object : TypeReference>() {}) + val map: Map = objectMapper.convertValue(entity, object : TypeReference>() {}) val keyFactory = parents.fold( initial = datastore.newKeyFactory().setKind(entityClass.qualifiedName) diff --git a/dataflow-pipelines/build.gradle.kts b/dataflow-pipelines/build.gradle.kts deleted file mode 100644 index c736922b3..000000000 --- a/dataflow-pipelines/build.gradle.kts +++ /dev/null @@ -1,55 +0,0 @@ -import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar -import org.gradle.api.tasks.testing.logging.TestExceptionFormat.FULL -import org.ostelco.prime.gradle.Version - -plugins { - kotlin("jvm") - application - id("com.github.johnrengelman.shadow") -} - -dependencies { - implementation(kotlin("stdlib-jdk8")) - - implementation("com.google.cloud:google-cloud-pubsub:${Version.googleCloudPubSub}") - - implementation("org.apache.beam:beam-sdks-java-core:${Version.beam}") - implementation("org.apache.beam:beam-runners-google-cloud-dataflow-java:${Version.beam}") - - implementation("ch.qos.logback:logback-classic:1.2.3") - - testRuntimeOnly("org.hamcrest:hamcrest-all:1.3") - testRuntimeOnly("org.apache.beam:beam-runners-direct-java:${Version.beam}") - - testImplementation("org.junit.jupiter:junit-jupiter-api:${Version.junit5}") - testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:${Version.junit5}") -} - -application { - mainClassName = "org.ostelco.dataflow.pipelines.DeployPipelineKt" -} - -tasks.withType { - mergeServiceFiles() - archiveClassifier.set("uber") - archiveVersion.set("") -} - -java { - sourceCompatibility = JavaVersion.VERSION_1_8 - targetCompatibility = JavaVersion.VERSION_1_8 -} - -tasks.test { - - // native support to Junit5 in Gradle 4.6+ - useJUnitPlatform { - includeEngines("junit-jupiter") - } - testLogging { - exceptionFormat = FULL - events("PASSED", "FAILED", "SKIPPED") - } -} - -apply(from = "../gradle/jacoco.gradle") \ No newline at end of file diff --git a/diameter-ha/build.gradle.kts b/diameter-ha/build.gradle.kts index 5c9e26b0e..105d4934d 100644 --- a/diameter-ha/build.gradle.kts +++ b/diameter-ha/build.gradle.kts @@ -18,11 +18,11 @@ dependencies { } implementation("org.slf4j:log4j-over-slf4j:${Version.slf4j}") - compile("io.lettuce:lettuce-core:5.1.8.RELEASE") + compile("io.lettuce:lettuce-core:5.2.0.RELEASE") testImplementation(kotlin("test-junit")) testRuntimeOnly("org.hamcrest:hamcrest-all:1.3") testImplementation("org.mockito:mockito-all:1.10.19") } -apply(from = "../gradle/jacoco.gradle") \ No newline at end of file +apply(from = "../gradle/jacoco.gradle.kts") \ No newline at end of file diff --git a/diameter-ha/src/main/kotlin/org/ostelco/diameter/ha/timer/ReplicatedTimerTaskData.kt b/diameter-ha/src/main/kotlin/org/ostelco/diameter/ha/timer/ReplicatedTimerTaskData.kt index 4f496ccde..ad91b2d43 100644 --- a/diameter-ha/src/main/kotlin/org/ostelco/diameter/ha/timer/ReplicatedTimerTaskData.kt +++ b/diameter-ha/src/main/kotlin/org/ostelco/diameter/ha/timer/ReplicatedTimerTaskData.kt @@ -72,9 +72,18 @@ class ReplicatedTimerTask(val data: ReplicatedTimerTaskData, private val session logger.debug("Task with id ${data.taskID} is recurring, not removing it") } - logger.debug("Firing Timer with id ${data.taskID}") - - runTask() + /* The TCC_CCASERVER_TIMER is supposed to clear any reservation and set state back to IDLE + We do not store the reservation in the gateway, therefore we do not need to trigger the + onTimer imlp. But we should clear any internals kept for this session to not waste memory. + */ + if (!data.taskID.toString().endsWith("TCC_CCASERVER_TIMER")) { + logger.debug("Firing Timer with id ${data.taskID}") + runTask() + } else { + // clear local session. As the timer was created by this instance it should be local. + logger.debug("Skipping Timer with id ${data.taskID}, removing session ${data.sessionId}") + sessionDataSource.removeSession(data.sessionId) + } } private fun removeFromScheduler() { diff --git a/diameter-stack/build.gradle.kts b/diameter-stack/build.gradle.kts index 97d397e90..bafde004e 100644 --- a/diameter-stack/build.gradle.kts +++ b/diameter-stack/build.gradle.kts @@ -30,4 +30,4 @@ dependencies { testImplementation("org.mockito:mockito-all:1.10.19") } -apply(from = "../gradle/jacoco.gradle") \ No newline at end of file +apply(from = "../gradle/jacoco.gradle.kts") \ No newline at end of file diff --git a/diameter-stack/src/main/kotlin/org/ostelco/diameter/model/Model.kt b/diameter-stack/src/main/kotlin/org/ostelco/diameter/model/Model.kt index 0b3281da6..282d15b66 100644 --- a/diameter-stack/src/main/kotlin/org/ostelco/diameter/model/Model.kt +++ b/diameter-stack/src/main/kotlin/org/ostelco/diameter/model/Model.kt @@ -82,6 +82,12 @@ class ServiceUnit() { @AvpField(Avp.CC_OUTPUT_OCTETS) var output: Long = 0 + @AvpField(Avp.CC_TIME) + var ccTime: Long = 0 + + @AvpField(Avp.CC_SERVICE_SPECIFIC_UNITS) + var ccServiceSpecificUnits: Long = 0 + @AvpField(Avp.REPORTING_REASON) var reportingReason: ReportingReason? = null @@ -106,8 +112,8 @@ class MultipleServiceCreditControl() { @AvpList(Avp.REQUESTED_SERVICE_UNIT, ServiceUnit::class) var requested: List = emptyList() - @AvpField(Avp.USED_SERVICE_UNIT) - var used = ServiceUnit() + @AvpList(Avp.USED_SERVICE_UNIT, ServiceUnit::class) + var used: List = emptyList() @AvpField(Avp.GRANTED_SERVICE_UNIT) var granted = ServiceUnit() @@ -130,7 +136,7 @@ class MultipleServiceCreditControl() { ratingGroup: Long, serviceIdentifier: Long, requested: List, - used: ServiceUnit, + used: List, granted: ServiceUnit, validityTime: Int, quotaHoldingTime: Long, diff --git a/diameter-test/src/main/kotlin/org/ostelco/diameter/test/TestClient.kt b/diameter-test/src/main/kotlin/org/ostelco/diameter/test/TestClient.kt index 641bc6eed..c563c0551 100644 --- a/diameter-test/src/main/kotlin/org/ostelco/diameter/test/TestClient.kt +++ b/diameter-test/src/main/kotlin/org/ostelco/diameter/test/TestClient.kt @@ -180,11 +180,11 @@ class TestClient : EventListener { } fun getAnswer(sessionId: String) : Result? { - return answerMap.get(sessionId) + return answerMap.remove(sessionId) } fun getRequest(sessionId: String) : Result? { - return requestMap.get(sessionId) + return requestMap.remove(sessionId) } override fun receivedSuccessMessage(request: Request, answer: Answer) { diff --git a/diameter-test/src/main/kotlin/org/ostelco/diameter/test/TestHelper.kt b/diameter-test/src/main/kotlin/org/ostelco/diameter/test/TestHelper.kt index 71f8a0647..0b44eb6ed 100644 --- a/diameter-test/src/main/kotlin/org/ostelco/diameter/test/TestHelper.kt +++ b/diameter-test/src/main/kotlin/org/ostelco/diameter/test/TestHelper.kt @@ -42,7 +42,7 @@ object TestHelper { } } - private fun addBucketRequest(ccrAvps: AvpSet, ratingGroup: Int, serviceIdentifier: Int, requestedBucketSize: Long, usedBucketSize: Long = 0) { + private fun addBucketRequest(ccrAvps: AvpSet, ratingGroup: Int, serviceIdentifier: Int, requestedBucketSize: Long, usedBucketSize: Long = 0, ccTime: Long = 0, ccServiceSpecificUnits: Long = 0, reportingReason: ReportingReason = ReportingReason.QUOTA_EXHAUSTED) { set(ccrAvps) { @@ -69,7 +69,21 @@ object TestHelper { if (usedBucketSize > 0) { group(Avp.USED_SERVICE_UNIT) { avp(Avp.CC_TOTAL_OCTETS, usedBucketSize, pFlag = true) - avp(Avp.REPORTING_REASON, ReportingReason.QUOTA_EXHAUSTED.ordinal, VENDOR_ID_3GPP, mFlag = true, pFlag = true) + avp(Avp.REPORTING_REASON, reportingReason, VENDOR_ID_3GPP, mFlag = true, pFlag = true) + } + } + + if (ccTime > 0) { + group(Avp.USED_SERVICE_UNIT) { + avp(Avp.CC_TIME, ccTime, pFlag = true) + avp(Avp.REPORTING_REASON, ReportingReason.OTHER_QUOTA_TYPE.ordinal, VENDOR_ID_3GPP, mFlag = true, pFlag = true) + } + } + + if (ccServiceSpecificUnits > 0) { + group(Avp.USED_SERVICE_UNIT) { + avp(Avp.CC_SERVICE_SPECIFIC_UNITS, ccServiceSpecificUnits, pFlag = true) + avp(Avp.REPORTING_REASON, ReportingReason.OTHER_QUOTA_TYPE.ordinal, VENDOR_ID_3GPP, mFlag = true, pFlag = true) } } } @@ -136,7 +150,12 @@ object TestHelper { } } - + @JvmStatic + fun createInitRequest(ccrAvps: AvpSet, msisdn: String) { + buildBasicRequest(ccrAvps, RequestType.INITIAL_REQUEST, requestNumber = 0) + addUser(ccrAvps, msisdn = msisdn, imsi = IMSI) + addServiceInformation(ccrAvps, apn = APN, sgsnMccMnc = SGSN_MCC_MNC) + } @JvmStatic fun createInitRequest(ccrAvps: AvpSet, msisdn: String, requestedBucketSize: Long, ratingGroup: Int, serviceIdentifier: Int) { @@ -157,13 +176,21 @@ object TestHelper { } @JvmStatic - fun createUpdateRequest(ccrAvps: AvpSet, msisdn: String, requestedBucketSize: Long, usedBucketSize: Long, ratingGroup: Int, serviceIdentifier: Int) { + fun createUpdateRequest(ccrAvps: AvpSet, msisdn: String, requestedBucketSize: Long, usedBucketSize: Long, ratingGroup: Int, serviceIdentifier: Int, reportingReason: ReportingReason) { buildBasicRequest(ccrAvps, RequestType.UPDATE_REQUEST, requestNumber = 1) addUser(ccrAvps, msisdn = msisdn, imsi = IMSI) addBucketRequest(ccrAvps, ratingGroup, serviceIdentifier, requestedBucketSize = requestedBucketSize, usedBucketSize = usedBucketSize) addServiceInformation(ccrAvps, apn = APN, sgsnMccMnc = SGSN_MCC_MNC) } + @JvmStatic + fun createUpdateRequest(ccrAvps: AvpSet, msisdn: String, requestedBucketSize: Long, usedBucketSize: Long, ratingGroup: Int, serviceIdentifier: Int, ccTime: Long, ccServiceSpecificUnits: Long) { + buildBasicRequest(ccrAvps, RequestType.UPDATE_REQUEST, requestNumber = 1) + addUser(ccrAvps, msisdn = msisdn, imsi = IMSI) + addBucketRequest(ccrAvps, ratingGroup, serviceIdentifier, requestedBucketSize = requestedBucketSize, usedBucketSize = usedBucketSize, ccTime = ccTime, ccServiceSpecificUnits = ccServiceSpecificUnits) + addServiceInformation(ccrAvps, apn = APN, sgsnMccMnc = SGSN_MCC_MNC) + } + @JvmStatic fun createUpdateRequestFinal(ccrAvps: AvpSet, msisdn: String, usedBucketSize: Long, ratingGroup: Int, serviceIdentifier: Int) { buildBasicRequest(ccrAvps, RequestType.UPDATE_REQUEST, requestNumber = 1) diff --git a/docs/DEPLOY.md b/docs/DEPLOY.md index afcb81517..c7c10bef5 100644 --- a/docs/DEPLOY.md +++ b/docs/DEPLOY.md @@ -1,12 +1,9 @@ # Deploy to production -## Deploy OCSgw to GCP +## Deploy OCS gateway to GCP ./ocsgw/infra/script/deploy-ocsgw.sh -The script takes to parameters. First parameter is instance number [1/2/3]. Second parameter is environment [dev/prod]. -If no parameters passed it will deploy all instances in dev environment. - ## Deploy to kubernetes cluster on GCP Set env variable diff --git a/docs/LOGS.md b/docs/LOGS.md index 81b0429e4..13a1b8a2a 100644 --- a/docs/LOGS.md +++ b/docs/LOGS.md @@ -20,11 +20,11 @@ resource.labels.container_name="prime" * GKE container > private-cluster > All namespace_id * You can expand a single log and filter to log prime-only logs. -# OCSGW logs in GCP +# OCS Gateway logs in GCP Same steps as above. Use the filter below: ```properties -resource.type="global" +resource.type="gce_instance" logName="projects/GCP_PROJECT_ID/logs/ocsgw" ``` \ No newline at end of file diff --git a/docs/TODO.md b/docs/TODO.md deleted file mode 100644 index 68de918a5..000000000 --- a/docs/TODO.md +++ /dev/null @@ -1,24 +0,0 @@ -# TODO - - -* Start using something other than this file to track tasks. -* Get rid of the interface/impl pattern that is present in - the entities package. Since we simplified the data classes - with lombok, that separation gives little or no benefit. -* The interactions between the various types of messages are - confusing. Consider autogenerating sequence diagrams when - running tests to help document what is going on. -* Increase unit testability, restructure to make almost everything - unit testable. -* Refactor firebase database into something that is integration testable. -* Make a template project for dropwizard. -* Automatically generate javadoc in Travis build. -* Automatically publish javadoc to the github website. - https://github.com/blog/2233-publish-your-project-documentation-with-github-pages -* Look into making a healthcheck for firebase/firestore - - https://www.firebase.com/docs/web/guide/offline-capabilities.html#section-connection-state - this.firebaseDatabase.getReference("/.info/connected").addValueEventListener() - -* This looks like a good writeup of best (&worst) practices for testing - http://blog.codepipes.com/testing/software-testing-antipatterns.html We should - absorb this and adapt and institutionalise the practices we want to use. \ No newline at end of file diff --git a/docs/domain-model/classes.puml b/docs/domain-model/classes.puml index 7242090ef..eaf2ee6ee 100644 --- a/docs/domain-model/classes.puml +++ b/docs/domain-model/classes.puml @@ -10,6 +10,15 @@ class CustomerRegion { + kycStatusMap: [KycType, KycStatus] } +class Identity { + + id: String + + type: String +} + +class Identifies { + + provider: String +} + class Customer { + customerId: UUID + nickname: String @@ -55,7 +64,23 @@ class Product { + price: Price } +class PurchaseRecord { + + id: UUID + + timestamp: Long +} + +class Plan { + + id: String + + stripePlanId: String? + + stripeProductId: String? + + interval: String + + intervalCount: Long +} + +Identity "1" -- "1" Identifies +Customer "1" -- "1" Identifies Customer "1" -- "*" Customer + Customer "1" -- "*" CustomerRegion Region "1" -- "*" CustomerRegion Customer "1" -- "*" Bundle @@ -70,8 +95,9 @@ Offer "*" -- "*" Product Product "1" -- "1" Price Product "*" -- "1" ProductClass -Purchase "*" -- "1" Product -Customer "1" -- "*" Purchase -Purchase "1" -- "1" Payment +PurchaseRecord "*" -- "1" Product +Customer "1" -- "*" PurchaseRecord +PurchaseRecord "1" -- "1" Payment +Customer "1" -- "*" Plan @enduml diff --git a/docs/domain-model/classes.svg b/docs/domain-model/classes.svg index d3c810693..654deac71 100644 --- a/docs/domain-model/classes.svg +++ b/docs/domain-model/classes.svg @@ -1,4 +1,4 @@ -RegionregionCode: StringregionName: StringCustomerRegionstatus: CustomerRegionStatuskycStatusMap: [KycType, KycStatus]CustomercustomerId: UUIDnickname: StringcontactEmail: EmailanalyticsId: UUIDreferralId: UUIDgetAvailableProducts():[ProductID]Bundlebalance: LongSimProfileICCID: Stringstatus: Stringalias: StringSubscriptionMSISDN: StringSegmentVisible to Admin OnlyOfferVisible to Admin OnlyProductClassid: UUIDpath: StringPricecurrency: Stringamount: IntProductSKU: UUIDprice: PricePurchasePayment1*1*1*1**11**1********11*1*11*11RegionregionCode: StringregionName: StringCustomerRegionstatus: CustomerRegionStatuskycStatusMap: [KycType, KycStatus]CustomercustomerId: UUIDnickname: StringcontactEmail: EmailanalyticsId: UUIDreferralId: UUIDgetAvailableProducts():[ProductID]Bundlebalance: LongSimProfileICCID: Stringstatus: Stringalias: StringSubscriptionMSISDN: StringSegmentVisible to Admin OnlyOfferVisible to Admin OnlyProductClassid: UUIDpath: StringPricecurrency: Stringamount: IntProductSKU: UUIDprice: PricePurchasePayment1*1*1*1**11**1********11*1*11*11 \ No newline at end of file diff --git a/docs/domain-model/graph.svg b/docs/domain-model/graph.svg new file mode 100644 index 000000000..5fcd5ff7b --- /dev/null +++ b/docs/domain-model/graph.svg @@ -0,0 +1,217 @@ + + Neo4j Graph Visualization + Created using Neo4j (http://www.neo4j.com/) + + + + IDENTIFIES + + + + + FOR_PURCHASE_OF + + + + + FOR_PURCHASE_BY + + + + + SIM_PROFILE_FOR_REGION + + + + + OFFER_HAS_PRODUCT + + + + + OFFERED_TO_SEGMENT + + + + + BELONG_TO_SEGMENT + + + + + SUBSCRIBES_TO_PLAN + + + + + HAS_SUBSCRIPTION + + + + + HAS_SIM_PROFILE + + + + + BELONG_TO_REGION + + + + + EKYC_SCAN + + + + + HAS_BUNDLE + + + + + REFERRED + + + + + LINKED_TO_BUNDLE + + + + + SUBSCRIPTION_UNDER_SIM_PROFILE + + + + + + + + + Segment + + + + + + + Identity + + + + + + + Bundle + + + + + + + Purchas… + + + + + + + SimProfile + + + + + + + Offer + + + + + + + Product + + + + + + + Region + + + + + + + Customer + + + + + + + ScanInf… + + + + + + + Subscrip… + + + + + + + Plan + + + + \ No newline at end of file diff --git a/document-data-store/build.gradle.kts b/document-data-store/build.gradle.kts index 2d275e1bd..6c13f3ccd 100644 --- a/document-data-store/build.gradle.kts +++ b/document-data-store/build.gradle.kts @@ -11,4 +11,4 @@ dependencies { testImplementation(kotlin("test-junit")) } -apply(from = "../gradle/jacoco.gradle") \ No newline at end of file +apply(from = "../gradle/jacoco.gradle.kts") \ No newline at end of file diff --git a/document-data-store/src/main/kotlin/org/ostelco/prime/storage/documentstore/DocumentDataStore.kt b/document-data-store/src/main/kotlin/org/ostelco/prime/storage/documentstore/DocumentDataStore.kt index ff754e309..413244f4f 100644 --- a/document-data-store/src/main/kotlin/org/ostelco/prime/storage/documentstore/DocumentDataStore.kt +++ b/document-data-store/src/main/kotlin/org/ostelco/prime/storage/documentstore/DocumentDataStore.kt @@ -36,6 +36,12 @@ object DocumentDataStoreSingleton : DocumentStore { .getOrElse { emptyList() } override fun addNotificationToken(customerId: String, token: ApplicationToken): Boolean { + // Remove any other entries with the same token. + getNotificationTokens(customerId).forEach { + if (it.tokenType == token.tokenType && it.token == token.token && it.applicationID != token.applicationID) { + removeNotificationToken(customerId, it.applicationID) + } + } return notificationTokenStore.put( token, token.applicationID, diff --git a/ekyc/build.gradle.kts b/ekyc/build.gradle.kts index a4cf59eb6..3d04acc71 100644 --- a/ekyc/build.gradle.kts +++ b/ekyc/build.gradle.kts @@ -37,4 +37,4 @@ tasks.test { } } -apply(from = "../gradle/jacoco.gradle") \ No newline at end of file +apply(from = "../gradle/jacoco.gradle.kts") \ No newline at end of file diff --git a/email-notifier/build.gradle.kts b/email-notifier/build.gradle.kts index f42376003..af10f2800 100644 --- a/email-notifier/build.gradle.kts +++ b/email-notifier/build.gradle.kts @@ -35,4 +35,4 @@ tasks.test { } } -apply(from = "../gradle/jacoco.gradle") \ No newline at end of file +apply(from = "../gradle/jacoco.gradle.kts") \ No newline at end of file diff --git a/ext-auth-provider/Dockerfile b/ext-auth-provider/Dockerfile index 04433f967..ef6da7753 100644 --- a/ext-auth-provider/Dockerfile +++ b/ext-auth-provider/Dockerfile @@ -1,4 +1,4 @@ -FROM azul/zulu-openjdk:12.0.2 +FROM azul/zulu-openjdk:13 LABEL maintainer="dev@redotter.sg" diff --git a/ext-auth-provider/build.gradle.kts b/ext-auth-provider/build.gradle.kts index 383623ea8..e74ace227 100644 --- a/ext-auth-provider/build.gradle.kts +++ b/ext-auth-provider/build.gradle.kts @@ -34,4 +34,4 @@ tasks.withType { archiveVersion.set("") } -apply(from = "../gradle/jacoco.gradle") \ No newline at end of file +apply(from = "../gradle/jacoco.gradle.kts") \ No newline at end of file diff --git a/ext-myinfo-emulator/Dockerfile b/ext-myinfo-emulator/Dockerfile index 3a665b845..17be238cd 100644 --- a/ext-myinfo-emulator/Dockerfile +++ b/ext-myinfo-emulator/Dockerfile @@ -1,4 +1,4 @@ -FROM azul/zulu-openjdk:12.0.2 +FROM azul/zulu-openjdk:13 LABEL maintainer="dev@redotter.sg" diff --git a/ext-myinfo-emulator/build.gradle.kts b/ext-myinfo-emulator/build.gradle.kts index 84dff7ab6..d4d3b3c33 100644 --- a/ext-myinfo-emulator/build.gradle.kts +++ b/ext-myinfo-emulator/build.gradle.kts @@ -35,4 +35,4 @@ tasks.withType { archiveVersion.set("") } -apply(from = "../gradle/jacoco.gradle") \ No newline at end of file +apply(from = "../gradle/jacoco.gradle.kts") \ No newline at end of file diff --git a/ext-myinfo-emulator/src/main/kotlin/org/ostelco/ext/myinfo/JsonUtils.kt b/ext-myinfo-emulator/src/main/kotlin/org/ostelco/ext/myinfo/JsonUtils.kt index cc9c3f4ab..71392c3a9 100644 --- a/ext-myinfo-emulator/src/main/kotlin/org/ostelco/ext/myinfo/JsonUtils.kt +++ b/ext-myinfo-emulator/src/main/kotlin/org/ostelco/ext/myinfo/JsonUtils.kt @@ -5,5 +5,5 @@ import org.ostelco.prime.jsonmapper.objectMapper object JsonUtils { fun compactJson(json: String): String = objectMapper.writeValueAsString( - objectMapper.readValue>(json, object : TypeReference>() {})) + objectMapper.readValue>(json, object : TypeReference>() {})) } \ No newline at end of file diff --git a/github.com/go.mod b/github.com/go.mod deleted file mode 100644 index 3ba5b2de9..000000000 --- a/github.com/go.mod +++ /dev/null @@ -1,3 +0,0 @@ -module github.com/ostelco-core - -go 1.12 diff --git a/go.mod b/go.mod new file mode 100644 index 000000000..8ea2428ce --- /dev/null +++ b/go.mod @@ -0,0 +1,9 @@ +module github.com/ostelco/ostelco-core + +go 1.13 + +require ( + github.com/google/go-cmp v0.3.1 // indirect + github.com/pkg/errors v0.8.1 // indirect + gotest.tools v2.2.0+incompatible +) diff --git a/go.sum b/go.sum new file mode 100644 index 000000000..802d8ce53 --- /dev/null +++ b/go.sum @@ -0,0 +1,6 @@ +github.com/google/go-cmp v0.3.1 h1:Xye71clBPdm5HgqGwUkwhbynsUJZhDbS20FvLhQ2izg= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo= +gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw= diff --git a/gradle/jacoco.gradle b/gradle/jacoco.gradle deleted file mode 100644 index 1f506a14f..000000000 --- a/gradle/jacoco.gradle +++ /dev/null @@ -1,11 +0,0 @@ -jacocoTestReport { - group = "Reporting" - description = "Generate Jacoco coverage reports after running tests." - getAdditionalSourceDirs().from(files(sourceSets.main.allJava.srcDirs)) - reports { - xml.enabled = true - html.enabled = true - } -} - -check.dependsOn jacocoTestReport \ No newline at end of file diff --git a/gradle/jacoco.gradle.kts b/gradle/jacoco.gradle.kts new file mode 100644 index 000000000..22a95747a --- /dev/null +++ b/gradle/jacoco.gradle.kts @@ -0,0 +1,15 @@ +tasks { + named("jacocoTestReport") { + group = "Reporting" + description = "Generate Jacoco coverage reports after running tests." + additionalSourceDirs.from(files(project.the()["main"].allJava.srcDirs)) + reports { + xml.isEnabled = true + csv.isEnabled = false + html.isEnabled = true + html.destination = file("${buildDir}/jacocoHtml") + } + } +} + +tasks.named("check").get().dependsOn(tasks.named("jacocoTestReport")) \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 770f4bacf..92524eeb7 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ #Mon May 27 14:33:41 CEST 2019 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.2-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-6.0-rc-1-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/graphql/build.gradle.kts b/graphql/build.gradle.kts index c078d40f9..3825d11a6 100644 --- a/graphql/build.gradle.kts +++ b/graphql/build.gradle.kts @@ -11,10 +11,17 @@ dependencies { implementation("com.graphql-java:graphql-java:13.0") testImplementation("io.dropwizard:dropwizard-testing:${Version.dropwizard}") + testImplementation("org.mockito:mockito-core:${Version.mockito}") + testImplementation("net.bytebuddy:byte-buddy:${Version.byteBuddy}") { + because("mockito-kotlin:2.2.0 has byte-buddy:1.9.0 which does not work for java13") + } + testImplementation("net.bytebuddy:byte-buddy-agent:${Version.byteBuddy}") { + because("mockito-kotlin:2.2.0 has byte-buddy:1.9.0 which does not work for java13") + } testImplementation("io.jsonwebtoken:jjwt-api:${Version.jjwt}") testRuntimeOnly("io.jsonwebtoken:jjwt-impl:${Version.jjwt}") testRuntimeOnly("io.jsonwebtoken:jjwt-jackson:${Version.jjwt}") } -apply(from = "../gradle/jacoco.gradle") \ No newline at end of file +apply(from = "../gradle/jacoco.gradle.kts") \ No newline at end of file diff --git a/graphql/src/test/resources/customer.graphqls b/graphql/src/test/resources/customer.graphqls index 162cd3548..f057b6a45 100644 --- a/graphql/src/test/resources/customer.graphqls +++ b/graphql/src/test/resources/customer.graphqls @@ -7,11 +7,11 @@ type QueryType { } type Context { - customer: Customer! - bundles: [Bundle!] - regions(regionCode: String): [RegionDetails!] - products: [Product!] - purchases: [Purchase!] + customer: Customer + bundles: [Bundle!]! + regions(regionCode: String): [RegionDetails!]! + products: [Product!]! + purchases: [Purchase!]! } type Customer { @@ -29,8 +29,8 @@ type Bundle { type RegionDetails { region: Region! - status: CustomerRegionStatus - kycStatusMap: KycStatusMap + status: CustomerRegionStatus! + kycStatusMap: KycStatusMap! simProfiles: [SimProfile!] } @@ -42,6 +42,7 @@ type Region { enum CustomerRegionStatus { PENDING APPROVED + AVAILABLE } type KycStatusMap { @@ -75,8 +76,8 @@ enum SimProfileStatus { type Product { sku: String! price: Price! - properties: Properties - presentation: Presentation + properties: Properties! + presentation: Presentation! } type Properties { @@ -87,11 +88,11 @@ type Properties { type Presentation { subTotal: String payeeLabel: String - priceLabel: String + priceLabel: String! taxLabel: String tax: String subTotalLabel: String - productLabel: String + productLabel: String! label: String } @@ -104,4 +105,4 @@ type Purchase { id: String! product: Product! timestamp: Long! -} \ No newline at end of file +} diff --git a/houston/.gitignore b/houston/.gitignore index c6e369dd4..498c6f439 100644 --- a/houston/.gitignore +++ b/houston/.gitignore @@ -10,6 +10,7 @@ /build .firebase/*.cache +.vscode/ # misc .DS_Store diff --git a/houston/package.json b/houston/package.json index 0d948bcee..42a696bdb 100644 --- a/houston/package.json +++ b/houston/package.json @@ -3,32 +3,33 @@ "version": "0.1.0", "private": true, "dependencies": { - "@fortawesome/fontawesome-svg-core": "^1.2.19", - "@fortawesome/free-solid-svg-icons": "^5.9.0", - "@fortawesome/react-fontawesome": "^0.1.4", + "@fortawesome/fontawesome-svg-core": "^1.2.25", + "@fortawesome/free-solid-svg-icons": "^5.11.2", + "@fortawesome/react-fontawesome": "^0.1.7", "auth0-js": "^9.8.2", "bootstrap": "^4.3.1", "classnames": "^2.2.6", "enzyme": "^3.10.0", - "enzyme-adapter-react-16": "^1.14.0", + "enzyme-adapter-react-16": "^1.15.1", "humps": "^2.0.1", - "jest": "24.8.0", - "jest-enzyme": "^7.0.2", + "jest": "24.9.0", + "jest-enzyme": "^7.1.1", "jquery": "^3.3.1", "lodash": "^4.17.14", "normalizr": "^3.3.0", - "popper.js": "^1.14.4", + "popper.js": "^1.16.0", "prop-types": "^15.6.2", - "react": "^16.8.6", - "react-dom": "^16.8.6", + "react": "^16.10.2", + "react-dom": "^16.10.2", + "react-highlight-words": "^0.16.0", "react-json-view": "^1.19.1", - "react-redux": "^7.0.1", - "react-router": "^5.0.0", - "react-router-dom": "^5.0.0", - "react-scripts": "3.1.1", - "react-table": "^6.10.0", - "react-test-renderer": "^16.6.3", - "reactstrap": "^8.0.1", + "react-redux": "^7.1.1", + "react-router": "^5.1.2", + "react-router-dom": "^5.1.2", + "react-scripts": "3.2.0", + "react-table": "^6.10.3", + "react-test-renderer": "^16.10.2", + "reactstrap": "^8.1.1", "redux": "^4.0.1", "redux-actions": "^2.6.4", "redux-devtools-extension": "^2.13.5", diff --git a/houston/src/actions/alert.actions.js b/houston/src/actions/alert.actions.js index 234f82655..71c569b43 100644 --- a/houston/src/actions/alert.actions.js +++ b/houston/src/actions/alert.actions.js @@ -15,8 +15,8 @@ const reducer = handleActions( [alertSuccess]: (state, { payload }) => { return { ...state, type: 'alert-success', message: payload }; }, - [alertError]: (state, { payload: { message } }) => { - return { ...state, type: 'alert-danger', message }; + [alertError]: (state, { payload: { message, code } }) => { + return { ...state, type: 'alert-danger', message, code }; }, [clearAlert]: () => defaultState }, diff --git a/houston/src/actions/cutomer.actions.js b/houston/src/actions/cutomer.actions.js new file mode 100644 index 000000000..a37c6e03f --- /dev/null +++ b/houston/src/actions/cutomer.actions.js @@ -0,0 +1,32 @@ +import { createActions, handleActions } from 'redux-actions'; + +const SELECT_CUSTOMER = 'SELECT_CUSTOMER'; +const CLEAR_CUSTOMER = 'CLEAR_CUSTOMER'; + +const defaultState = {}; + +const actions = createActions( + SELECT_CUSTOMER, + CLEAR_CUSTOMER); + +const { + selectCustomer, + clearCustomer, +} = actions; + + +export const customerActions = { ...actions }; + + const reducer = handleActions( + { + [selectCustomer]: (state, { payload }) => { + return { ...payload }; + }, + [clearCustomer]: () => { + return defaultState; + }, + }, + defaultState +); + +export default reducer; diff --git a/houston/src/actions/notifiy.actions.js b/houston/src/actions/notifiy.actions.js index c72099deb..e2369cfd3 100644 --- a/houston/src/actions/notifiy.actions.js +++ b/houston/src/actions/notifiy.actions.js @@ -3,8 +3,6 @@ import { alertActions } from './alert.actions'; import { CALL_API } from '../helpers/api'; import _ from 'lodash'; -import { encodeEmail } from '../helpers/utils'; - const NOTIFY_REQUEST = 'NOTIFY_REQUEST'; const NOTIFY_SUCCESS = 'NOTIFY_SUCCESS'; const NOTIFY_FAILURE = 'NOTIFY_FAILURE'; @@ -39,13 +37,13 @@ const { setNotificationType, } = actions; -const putNotificationByEmail = (email, title, message) => ({ +const putNotificationById = (id, title, message) => ({ [CALL_API]: { actions: [ actions.notifyRequest, actions.notifySuccess, actions.notifyFailure], - endpoint: `notify/${email}`, + endpoint: `notify/${id}`, method: 'PUT', allowEmptyResponse: true, params: { message, title } @@ -57,10 +55,11 @@ const sendNotificationToSubscriber = (title, message) => (dispatch, getState) => console.log('Error reported.', error); dispatch(alertActions.alertError(error)); }; - // Get the email from the fetched user - const subscriberEmail = encodeEmail(_.get(getState(), 'subscriber.contactEmail')); - if (subscriberEmail) { - return dispatch(putNotificationByEmail(subscriberEmail, title, message)) + + // Get the id from the fetched user + const subscriberId = _.get(getState(), 'customer.id'); + if (subscriberId) { + return dispatch(putNotificationById(subscriberId, title, message)) .catch(handleError); } }; diff --git a/houston/src/actions/subscriber.actions.js b/houston/src/actions/subscriber.actions.js index fcc2a85db..dd51761a2 100644 --- a/houston/src/actions/subscriber.actions.js +++ b/houston/src/actions/subscriber.actions.js @@ -3,6 +3,7 @@ import { createActions } from 'redux-actions'; import { CALL_API } from '../helpers/api'; import { alertActions } from './alert.actions'; +import { customerActions } from './cutomer.actions'; import { encodeEmail } from '../helpers/utils'; const SUBSCRIBER_BY_EMAIL_REQUEST = 'SUBSCRIBER_BY_EMAIL_REQUEST'; @@ -83,87 +84,88 @@ const fetchSubscriberById = (id) => ({ } }); -const fetchContextByEmail = (email) => ({ +const fetchContextById = (id) => ({ [CALL_API]: { actions: [ actions.contextByEmailRequest, actions.contextByEmailSuccess, actions.contextByEmailFailure], - endpoint: `context/${email}`, + endpoint: `context/${id}`, method: 'GET' } }); -const fetchSubscriptionsByEmail = (email) => ({ +const fetchSubscriptionsById = (id) => ({ [CALL_API]: { actions: [ actions.subscriptionsRequest, actions.subscriptionsSuccess, actions.subscriptionsFailure], - endpoint: `profiles/${email}/subscriptions`, + endpoint: `profiles/${id}/subscriptions`, method: 'GET' } }); -const fetchBundlesByEmail = (email) => ({ +const fetchBundlesById = (id) => ({ [CALL_API]: { actions: [ actions.bundlesRequest, actions.bundlesSuccess, actions.bundlesFailure], - endpoint: `bundles/${email}`, + endpoint: `bundles/${id}`, method: 'GET' } }); -const fetchPaymentHistoryByEmail = (email) => ({ +const fetchPaymentHistoryById = (id) => ({ [CALL_API]: { actions: [ actions.paymentHistoryRequest, actions.paymentHistorySuccess, actions.paymentHistoryFailure], - endpoint: `purchases/${email}`, + endpoint: `purchases/${id}`, method: 'GET' } }); -const putRefundPurchaseByEmail = (email, purchaseRecordId, reason) => ({ +const putRefundPurchaseById = (id, purchaseRecordId, reason) => ({ [CALL_API]: { actions: [ actions.refundPaymentRequest, actions.refundPaymentSuccess, actions.refundPaymentFailure], - endpoint: `refund/${email}`, + endpoint: `refund/${id}`, method: 'PUT', params: { purchaseRecordId, reason } } }); -const fetchAuditLogsByEmail = (email) => ({ +const fetchAuditLogsById = (id) => ({ [CALL_API]: { actions: [ actions.auditLogsRequest, actions.auditLogsSuccess, actions.auditLogsFailure], - endpoint: `auditLog/${email}`, + endpoint: `auditLog/${id}`, method: 'GET' } }); -const deleteUserByEmail = (email) => ({ +const deleteUserById = (id) => ({ [CALL_API]: { actions: [ actions.deleteUserRequest, actions.deleteUserSuccess, actions.deleteUserFailure], - endpoint: `customer/${email}`, + endpoint: `customer/${id}`, allowEmptyResponse: true, method: 'DELETE' } }); // TODO: API based implementaion. Reference: https://github.com/reduxjs/redux/issues/1676 -const getSubscriberAndBundlesByEmail = (email) => (dispatch, getState) => { +const getSubscriberList = (email) => (dispatch, getState) => { + dispatch(customerActions.clearCustomer()); localStorage.setItem('searchedEmail', email) email = encodeEmail(email); @@ -174,56 +176,67 @@ const getSubscriberAndBundlesByEmail = (email) => (dispatch, getState) => { return dispatch(fetchSubscriberById(email)) .then(() => { - // Get the email from the fetched user - const subscriberEmail = encodeEmail(_.get(getState(), 'subscriber.contactEmail')); - if (subscriberEmail) { - dispatch(fetchContextByEmail(subscriberEmail)).catch(handleError); - dispatch(fetchAuditLogsByEmail(subscriberEmail)).catch(handleError); - dispatch(fetchSubscriptionsByEmail(subscriberEmail)).catch(handleError); - return dispatch(fetchBundlesByEmail(subscriberEmail)) - .then(() => { - return dispatch(fetchPaymentHistoryByEmail(subscriberEmail)); - }) - .catch(handleError); + const subscribers = _.get(getState(), 'subscribers'); + if (Array.isArray(subscribers) && subscribers.length === 1) { + dispatch(selectCustomer(subscribers[0])); } }) .catch(handleError); }; -const refundPurchase = (purchaseRecordId, reason) => (dispatch, getState) => { +const selectCustomer = (customer) => (dispatch, getState) => { + dispatch(customerActions.selectCustomer(customer)); + const handleError = (error) => { + console.log('Error reported.', error); + dispatch(alertActions.alertError(error)); + }; + const customerId = _.get(getState(), 'customer.id');; + if (customerId) { + dispatch(fetchContextById(customerId)).catch(handleError); + dispatch(fetchAuditLogsById(customerId)).catch(handleError); + dispatch(fetchSubscriptionsById(customerId)).catch(handleError); + return dispatch(fetchBundlesById(customerId)) + .then(() => { + return dispatch(fetchPaymentHistoryById(customerId)); + }) + .catch(handleError); + } +}; + +const refundPurchase = (purchaseRecordId, reason) => (dispatch, getState) => { const handleError = (error) => { console.log('Error reported.', error); dispatch(alertActions.alertError(error)); }; - // Get the email from the fetched user - const subscriberEmail = encodeEmail(_.get(getState(), 'subscriber.contactEmail')); - if (subscriberEmail) { - return dispatch(putRefundPurchaseByEmail(subscriberEmail, purchaseRecordId, reason)) + // Get the id from the fetched user + const subscriberId = _.get(getState(), 'customer.id'); + if (subscriberId) { + return dispatch(putRefundPurchaseById(subscriberId, purchaseRecordId, reason)) .then(() => { - return dispatch(fetchPaymentHistoryByEmail(subscriberEmail)); + return dispatch(fetchPaymentHistoryById(subscriberId)); }) .catch(handleError); } }; const deleteUser = () => (dispatch, getState) => { - const handleError = (error) => { console.log('Error reported.', error.message); let message = "Failed to delete user (" +error.message+")" dispatch(alertActions.alertError({message})); }; - // Get the email from the fetched user - const subscriberEmail = encodeEmail(_.get(getState(), 'subscriber.contactEmail')); - if (subscriberEmail) { - return dispatch(deleteUserByEmail(subscriberEmail)) + // Get the id from the fetched user + const subscriberId = _.get(getState(), 'customer.id'); + if (subscriberId) { + return dispatch(deleteUserById(subscriberId)) .catch(handleError); } }; export const subscriberActions = { - getSubscriberAndBundlesByEmail, + getSubscriberList, + selectCustomer, refundPurchase, deleteUser }; diff --git a/houston/src/components/Search/Alert.js b/houston/src/components/Search/Alert.js index 0c14f7a6f..0521c6b27 100644 --- a/houston/src/components/Search/Alert.js +++ b/houston/src/components/Search/Alert.js @@ -4,6 +4,7 @@ import { connect } from 'react-redux'; import { Alert } from 'reactstrap'; import { alertActions } from '../../actions/alert.actions'; +import { authConstants } from '../../actions/auth.actions'; function AlertMessage(props) { function onDismiss(e) { @@ -12,8 +13,12 @@ function AlertMessage(props) { const visible = (props.alert && props.alert.type === 'alert-danger'); if (!visible) { - return null - }; + return null; + } + // Don't show Authentication failed message + if (props.alert.code === authConstants.AUTHENTICATION_FAILURE) { + return null; + } return ( {props.alert.message} diff --git a/houston/src/components/Search/AuditLogs.js b/houston/src/components/Search/AuditLogs.js index b6c131c2f..6dbbb29ea 100644 --- a/houston/src/components/Search/AuditLogs.js +++ b/houston/src/components/Search/AuditLogs.js @@ -2,7 +2,7 @@ import React from 'react'; import _ from 'lodash'; import { connect } from 'react-redux'; import PropTypes from 'prop-types'; -import { Card, CardBody, CardTitle } from 'reactstrap'; +import { Card, CardBody } from 'reactstrap'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' import ReactTable from "react-table" import "react-table/react-table.css"; @@ -76,7 +76,6 @@ class AuditLogs extends React.Component { return ( - Audit Logs ( + {children} +); + +const convertToHighlightedText = (text, query) => { + return ( + ); +} + +export const CustomerRow = props => { + + let query = localStorage.getItem('searchedEmail') + function onSelect(e) { + e.preventDefault(); + console.log(`Selecting customer with id ${props.customer.id}`); + props.selectCustomer(props.customer); + } + + return ( + + + + {convertToHighlightedText(props.customer.nickname, query)}
+ {convertToHighlightedText(props.customer.contactEmail, query)}
+ {convertToHighlightedText(props.customer.id, query)}
+
+ +
+
+
); +} + +CustomerRow.propTypes = { + customer: PropTypes.shape({ + id: PropTypes.string, + nickname: PropTypes.string, + contactEmail: PropTypes.string, + }), + selectCustomer: PropTypes.func.isRequired +}; + +export const CustomerList = props => { + // If customer is set, remove the list. + if (props.customer.id || !Array.isArray(props.subscribers)) { + return null; + } + let listItems = null; + if (Array.isArray(props.subscribers)) { + listItems = props.subscribers.map((customer, index) => +
+ +
+ +
+ ); + } + return ( +
+
Found following matching records...
+ {listItems} +
+ ); +} + +CustomerList.propTypes = { + subscribers: PropTypes.oneOfType([ + PropTypes.object, + PropTypes.array + ]), + customer: PropTypes.object, + selectCustomer: PropTypes.func.isRequired +}; + +function mapStateToProps(state) { + const { subscribers, customer } = state + + return { + subscribers, + customer + }; +} +const mapDispatchToProps = { + selectCustomer: subscriberActions.selectCustomer +} +export default connect(mapStateToProps, mapDispatchToProps)(CustomerList); diff --git a/houston/src/components/Search/DataUsage.js b/houston/src/components/Search/DataUsage.js index d2c8a64e0..41a63cbce 100644 --- a/houston/src/components/Search/DataUsage.js +++ b/houston/src/components/Search/DataUsage.js @@ -1,7 +1,7 @@ import React from 'react'; import { connect } from 'react-redux'; import PropTypes from 'prop-types'; -import { Col, Row, Card, CardBody, CardTitle, Button } from 'reactstrap'; +import { Col, Row, Card, CardBody, CardHeader } from 'reactstrap'; import WarningModal from '../Shared/WarningModal'; import { humanReadableBytes } from '../../helpers'; @@ -39,15 +39,15 @@ class DataUsage extends React.Component { } return ( + Data balance - Data balance {`Remaining ${props.balance}.`} - + {/* - + */} + Payment History - Payment History diff --git a/houston/src/components/Search/Profile.js b/houston/src/components/Search/Profile.js index ad72a29fd..e8265e767 100644 --- a/houston/src/components/Search/Profile.js +++ b/houston/src/components/Search/Profile.js @@ -1,7 +1,7 @@ import React from 'react'; import { connect } from 'react-redux'; import PropTypes from 'prop-types'; -import { Col, Row, Card, CardBody, CardTitle, Button } from 'reactstrap'; +import { Col, Row, Card, CardBody, CardHeader, Button } from 'reactstrap'; import { subscriberActions } from '../../actions/subscriber.actions'; import Subscription from './Subscription'; @@ -41,15 +41,15 @@ class Profile extends React.Component { if (Array.isArray(props.subscriptions.items)) { listItems = props.subscriptions.items.map((subscription, index) =>
- +
); } return ( + Customer - User Profile {'Name:'}{`${props.profile.nickname}`} @@ -92,14 +92,14 @@ Profile.propTypes = { subscriptions: PropTypes.shape({ items: PropTypes.array, }), - deleteUser:PropTypes.func.isRequired + deleteUser: PropTypes.func.isRequired }; function mapStateToProps(state) { - const { subscriber } = state; const { subscriptions } = state; + const { customer } = state return { - profile: subscriber, + profile: customer, subscriptions }; } diff --git a/houston/src/components/Search/Search.js b/houston/src/components/Search/Search.js index def31a8db..2b5985a93 100644 --- a/houston/src/components/Search/Search.js +++ b/houston/src/components/Search/Search.js @@ -1,25 +1,28 @@ import React from 'react'; import { connect } from 'react-redux'; import PropTypes from 'prop-types'; +import _ from 'lodash'; import { subscriberActions } from '../../actions/subscriber.actions'; import SearchForm from './SearchForm'; +import CustomerList from './CustomerList'; import SearchResults from './SearchResults'; import AlertMessage from './Alert'; class Search extends React.Component { onSubmit = (text) => { - this.props.getSubscriberAndBundlesByEmail(text); + this.props.getSubscriberList(text); } render() { - const hasResults = this.props.profile.nickname || false; + const hasResults = (this.props.profile && this.props.profile.nickname) || false; return (

+ { hasResults && ( @@ -38,7 +41,7 @@ Search.propTypes = { function mapStateToProps(state) { const { loggedIn } = state.authentication; - const { subscriber } = state; + const subscriber = _.get(state, 'customer') return { loggedIn, profile: subscriber @@ -46,6 +49,6 @@ function mapStateToProps(state) { }; const mapDispatchToProps = { - getSubscriberAndBundlesByEmail: subscriberActions.getSubscriberAndBundlesByEmail + getSubscriberList: subscriberActions.getSubscriberList }; export default connect(mapStateToProps, mapDispatchToProps)(Search); diff --git a/houston/src/components/Search/SearchForm.js b/houston/src/components/Search/SearchForm.js index 0a46adab6..aa30e15a3 100644 --- a/houston/src/components/Search/SearchForm.js +++ b/houston/src/components/Search/SearchForm.js @@ -1,6 +1,6 @@ import React, { useState } from 'react'; import PropTypes from 'prop-types'; -import { Button, Form, FormGroup, Label, Input } from 'reactstrap'; +import { Button, Col, Form, FormGroup, Input, Label, Row } from 'reactstrap'; import { getTextType } from '../../helpers'; @@ -8,7 +8,8 @@ function useFormInput(initialValue, submit) { const [value, setValue] = useState(initialValue); function onChange(e) { - setValue(e.target.value); + const cleanValue = e.target.value.replace(/['"]+/g,''); + setValue(cleanValue); } function onSubmit(e) { @@ -39,15 +40,21 @@ export default function SearchForm(props) {

- - + + +
+ + + + + + - ); diff --git a/houston/src/components/Search/SearchResults.js b/houston/src/components/Search/SearchResults.js index 9c88d56ef..2269bcf5b 100644 --- a/houston/src/components/Search/SearchResults.js +++ b/houston/src/components/Search/SearchResults.js @@ -1,5 +1,5 @@ import React from 'react'; -import { Card, CardBody, CardTitle, Nav, NavItem, NavLink, TabContent, TabPane } from 'reactstrap'; +import { Card, CardBody, CardHeader, Nav, NavItem, NavLink, TabContent, TabPane } from 'reactstrap'; import classnames from 'classnames'; import Context from "./Context"; @@ -63,13 +63,14 @@ class SearchResults extends React.Component { +


+ Push Notifications - Push Notifications
+
+
+
diff --git a/houston/src/components/Search/Subscription.js b/houston/src/components/Search/Subscription.js index afffb3618..1de5d40ed 100644 --- a/houston/src/components/Search/Subscription.js +++ b/houston/src/components/Search/Subscription.js @@ -1,6 +1,6 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { Col, Row, Button } from 'reactstrap'; +import { Col, Row } from 'reactstrap'; import WarningModal from '../Shared/WarningModal'; @@ -59,8 +59,7 @@ export default class Subscription extends React.Component { {'Phone number:'}{`${subscription.msisdn}`} -
- + {/* - + */} (next) => (action) => { @@ -134,7 +141,7 @@ export default (store) => (next) => (action) => { errorObj: error, error: transformError(error) })); - throw new Error(transformError(error)); + throw new ApiError(transformError(error), error.code); } ); } diff --git a/houston/src/reducers/index.js b/houston/src/reducers/index.js index 30620b424..0000096ab 100644 --- a/houston/src/reducers/index.js +++ b/houston/src/reducers/index.js @@ -7,11 +7,12 @@ import { notifyConstants } from '../actions/notifiy.actions'; // Reducers. import alert from '../actions/alert.actions'; +import customer from '../actions/cutomer.actions'; import notification from '../actions/notifiy.actions'; import authentication from './auth.reducer'; import { context, - subscriber, + subscribers, subscriptions, bundles, paymentHistory, @@ -23,7 +24,8 @@ const appReducer = combineReducers({ authentication, alert, notification, - subscriber, + subscribers, + customer, context, subscriptions, bundles, diff --git a/houston/src/reducers/subscriber.reducer.js b/houston/src/reducers/subscriber.reducer.js index fc7bcfa46..c12ab265a 100644 --- a/houston/src/reducers/subscriber.reducer.js +++ b/houston/src/reducers/subscriber.reducer.js @@ -3,14 +3,12 @@ import { actions } from '../actions/subscriber.actions'; const defaultState = {}; -export const subscriber = handleActions( +export const subscribers = handleActions( { [actions.subscriberByEmailRequest]: (state, action) => ({ loading: true }), - [actions.subscriberByEmailSuccess]: (state, action) => ({ - ...action.payload - }), + [actions.subscriberByEmailSuccess]: (state, action) => (action.payload), // Array of subscribers [actions.subscriberByEmailFailure]: (state, action) => ({ ...action.payload }) diff --git a/houston/yarn.lock b/houston/yarn.lock index ac6c25ff5..c4c89dfdc 100644 --- a/houston/yarn.lock +++ b/houston/yarn.lock @@ -9,18 +9,18 @@ dependencies: "@babel/highlight" "^7.0.0" -"@babel/core@7.5.5", "@babel/core@^7.1.0", "@babel/core@^7.4.5": - version "7.5.5" - resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.5.5.tgz#17b2686ef0d6bc58f963dddd68ab669755582c30" - integrity sha512-i4qoSr2KTtce0DmkuuQBV4AuQgGPUcPXMr9L5MyYAtk06z068lQ10a4O009fe5OB/DfNV+h+qqT7ddNV8UnRjg== +"@babel/core@7.6.0": + version "7.6.0" + resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.6.0.tgz#9b00f73554edd67bebc86df8303ef678be3d7b48" + integrity sha512-FuRhDRtsd6IptKpHXAa+4WPZYY2ZzgowkbLBecEDDSje1X/apG7jQM33or3NdOmjXBKWGOg4JmSiRfUfuTtHXw== dependencies: "@babel/code-frame" "^7.5.5" - "@babel/generator" "^7.5.5" - "@babel/helpers" "^7.5.5" - "@babel/parser" "^7.5.5" - "@babel/template" "^7.4.4" - "@babel/traverse" "^7.5.5" - "@babel/types" "^7.5.5" + "@babel/generator" "^7.6.0" + "@babel/helpers" "^7.6.0" + "@babel/parser" "^7.6.0" + "@babel/template" "^7.6.0" + "@babel/traverse" "^7.6.0" + "@babel/types" "^7.6.0" convert-source-map "^1.1.0" debug "^4.1.0" json5 "^2.1.0" @@ -29,16 +29,35 @@ semver "^5.4.1" source-map "^0.5.0" -"@babel/generator@^7.4.0", "@babel/generator@^7.5.5": - version "7.5.5" - resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.5.5.tgz#873a7f936a3c89491b43536d12245b626664e3cf" - integrity sha512-ETI/4vyTSxTzGnU2c49XHv2zhExkv9JHLTwDAFz85kmcwuShvYG2H08FwgIguQf4JC75CBnXAUM5PqeF4fj0nQ== +"@babel/core@^7.1.0", "@babel/core@^7.4.5": + version "7.6.4" + resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.6.4.tgz#6ebd9fe00925f6c3e177bb726a188b5f578088ff" + integrity sha512-Rm0HGw101GY8FTzpWSyRbki/jzq+/PkNQJ+nSulrdY6gFGOsNseCqD6KHRYe2E+EdzuBdr2pxCp6s4Uk6eJ+XQ== dependencies: - "@babel/types" "^7.5.5" + "@babel/code-frame" "^7.5.5" + "@babel/generator" "^7.6.4" + "@babel/helpers" "^7.6.2" + "@babel/parser" "^7.6.4" + "@babel/template" "^7.6.0" + "@babel/traverse" "^7.6.3" + "@babel/types" "^7.6.3" + convert-source-map "^1.1.0" + debug "^4.1.0" + json5 "^2.1.0" + lodash "^4.17.13" + resolve "^1.3.2" + semver "^5.4.1" + source-map "^0.5.0" + +"@babel/generator@^7.4.0", "@babel/generator@^7.6.0", "@babel/generator@^7.6.3", "@babel/generator@^7.6.4": + version "7.6.4" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.6.4.tgz#a4f8437287bf9671b07f483b76e3bb731bc97671" + integrity sha512-jsBuXkFoZxk0yWLyGI9llT9oiQ2FeTASmRFE32U+aaDTfoE92t78eroO7PTpU/OrYq38hlcDM6vbfLDaOLy+7w== + dependencies: + "@babel/types" "^7.6.3" jsesc "^2.5.1" lodash "^4.17.13" source-map "^0.5.0" - trim-right "^1.0.1" "@babel/helper-annotate-as-pure@^7.0.0": version "7.0.0" @@ -72,10 +91,10 @@ "@babel/traverse" "^7.4.4" "@babel/types" "^7.4.4" -"@babel/helper-create-class-features-plugin@^7.4.4", "@babel/helper-create-class-features-plugin@^7.5.5": - version "7.5.5" - resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.5.5.tgz#401f302c8ddbc0edd36f7c6b2887d8fa1122e5a4" - integrity sha512-ZsxkyYiRA7Bg+ZTRpPvB6AbOFKTFFK4LrvTet8lInm0V468MWCaSYJE+I7v2z2r8KNLtYiV+K5kTCnR7dvyZjg== +"@babel/helper-create-class-features-plugin@^7.5.5", "@babel/helper-create-class-features-plugin@^7.6.0": + version "7.6.0" + resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.6.0.tgz#769711acca889be371e9bc2eb68641d55218021f" + integrity sha512-O1QWBko4fzGju6VoVvrZg0RROCVifcLxiApnGP3OWfWzvxRZFCoBD81K5ur5e3bVY2Vf/5rIJm8cqPKn8HUJng== dependencies: "@babel/helper-function-name" "^7.1.0" "@babel/helper-member-expression-to-functions" "^7.5.5" @@ -215,14 +234,14 @@ "@babel/traverse" "^7.1.0" "@babel/types" "^7.2.0" -"@babel/helpers@^7.5.5": - version "7.5.5" - resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.5.5.tgz#63908d2a73942229d1e6685bc2a0e730dde3b75e" - integrity sha512-nRq2BUhxZFnfEn/ciJuhklHvFOqjJUD5wpx+1bxUF2axL9C+v4DE/dmp5sT2dKnpOs4orZWzpAZqlCy8QqE/7g== +"@babel/helpers@^7.6.0", "@babel/helpers@^7.6.2": + version "7.6.2" + resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.6.2.tgz#681ffe489ea4dcc55f23ce469e58e59c1c045153" + integrity sha512-3/bAUL8zZxYs1cdX2ilEE0WobqbCmKWr/889lf2SS0PpDcpEIY8pb1CCyz0pEcX3pEb+MCbks1jIokz2xLtGTA== dependencies: - "@babel/template" "^7.4.4" - "@babel/traverse" "^7.5.5" - "@babel/types" "^7.5.5" + "@babel/template" "^7.6.0" + "@babel/traverse" "^7.6.2" + "@babel/types" "^7.6.0" "@babel/highlight@^7.0.0": version "7.5.0" @@ -233,10 +252,10 @@ esutils "^2.0.2" js-tokens "^4.0.0" -"@babel/parser@^7.0.0", "@babel/parser@^7.1.0", "@babel/parser@^7.4.3", "@babel/parser@^7.4.4", "@babel/parser@^7.5.5": - version "7.5.5" - resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.5.5.tgz#02f077ac8817d3df4a832ef59de67565e71cca4b" - integrity sha512-E5BN68cqR7dhKan1SfqgPGhQ178bkVKpXTPEXnFJBrEt8/DKRZlybmy+IgYLTeN7tp1R5Ccmbm2rBk17sHYU3g== +"@babel/parser@^7.0.0", "@babel/parser@^7.1.0", "@babel/parser@^7.4.3", "@babel/parser@^7.6.0", "@babel/parser@^7.6.3", "@babel/parser@^7.6.4": + version "7.6.4" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.6.4.tgz#cb9b36a7482110282d5cb6dd424ec9262b473d81" + integrity sha512-D8RHPW5qd0Vbyo3qb+YjO5nvUVRTXFLQ/FsDxJU2Nqz4uB5EnUN0ZQSEYpvTIbRuttig1XbHWU5oMeQwQSAA+A== "@babel/plugin-proposal-async-generator-functions@^7.2.0": version "7.2.0" @@ -255,12 +274,12 @@ "@babel/helper-create-class-features-plugin" "^7.5.5" "@babel/helper-plugin-utils" "^7.0.0" -"@babel/plugin-proposal-decorators@7.4.4": - version "7.4.4" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-decorators/-/plugin-proposal-decorators-7.4.4.tgz#de9b2a1a8ab0196f378e2a82f10b6e2a36f21cc0" - integrity sha512-z7MpQz3XC/iQJWXH9y+MaWcLPNSMY9RQSthrLzak8R8hCj0fuyNk+Dzi9kfNe/JxxlWQ2g7wkABbgWjW36MTcw== +"@babel/plugin-proposal-decorators@7.6.0": + version "7.6.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-decorators/-/plugin-proposal-decorators-7.6.0.tgz#6659d2572a17d70abd68123e89a12a43d90aa30c" + integrity sha512-ZSyYw9trQI50sES6YxREXKu+4b7MAg6Qx2cvyDDYjP2Hpzd3FleOUwC9cqn1+za8d0A2ZU8SHujxFao956efUg== dependencies: - "@babel/helper-create-class-features-plugin" "^7.4.4" + "@babel/helper-create-class-features-plugin" "^7.6.0" "@babel/helper-plugin-utils" "^7.0.0" "@babel/plugin-syntax-decorators" "^7.2.0" @@ -280,7 +299,7 @@ "@babel/helper-plugin-utils" "^7.0.0" "@babel/plugin-syntax-json-strings" "^7.2.0" -"@babel/plugin-proposal-object-rest-spread@7.5.5", "@babel/plugin-proposal-object-rest-spread@^7.5.5": +"@babel/plugin-proposal-object-rest-spread@7.5.5": version "7.5.5" resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.5.5.tgz#61939744f71ba76a3ae46b5eea18a54c16d22e58" integrity sha512-F2DxJJSQ7f64FyTVl5cw/9MWn6naXGdk3Q3UhDbFEEHv+EilCPoeRD3Zh/Utx1CJz4uyKlQ4uH+bJPbEhMV7Zw== @@ -288,6 +307,14 @@ "@babel/helper-plugin-utils" "^7.0.0" "@babel/plugin-syntax-object-rest-spread" "^7.2.0" +"@babel/plugin-proposal-object-rest-spread@^7.5.5", "@babel/plugin-proposal-object-rest-spread@^7.6.2": + version "7.6.2" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.6.2.tgz#8ffccc8f3a6545e9f78988b6bf4fe881b88e8096" + integrity sha512-LDBXlmADCsMZV1Y9OQwMc0MyGZ8Ta/zlD9N67BfQT8uYwkRswiu2hU6nJKrjrt/58aH/vqfQlR/9yId/7A2gWw== + dependencies: + "@babel/helper-plugin-utils" "^7.0.0" + "@babel/plugin-syntax-object-rest-spread" "^7.2.0" + "@babel/plugin-proposal-optional-catch-binding@^7.2.0": version "7.2.0" resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-optional-catch-binding/-/plugin-proposal-optional-catch-binding-7.2.0.tgz#135d81edb68a081e55e56ec48541ece8065c38f5" @@ -296,14 +323,14 @@ "@babel/helper-plugin-utils" "^7.0.0" "@babel/plugin-syntax-optional-catch-binding" "^7.2.0" -"@babel/plugin-proposal-unicode-property-regex@^7.4.4": - version "7.4.4" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-unicode-property-regex/-/plugin-proposal-unicode-property-regex-7.4.4.tgz#501ffd9826c0b91da22690720722ac7cb1ca9c78" - integrity sha512-j1NwnOqMG9mFUOH58JTFsA/+ZYzQLUZ/drqWUqxCYLGeu2JFZL8YrNC9hBxKmWtAuOCHPcRpgv7fhap09Fb4kA== +"@babel/plugin-proposal-unicode-property-regex@^7.4.4", "@babel/plugin-proposal-unicode-property-regex@^7.6.2": + version "7.6.2" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-unicode-property-regex/-/plugin-proposal-unicode-property-regex-7.6.2.tgz#05413762894f41bfe42b9a5e80919bd575dcc802" + integrity sha512-NxHETdmpeSCtiatMRYWVJo7266rrvAC3DTeG5exQBIH/fMIUK7ejDNznBbn3HQl/o9peymRRg7Yqkx6PdUXmMw== dependencies: "@babel/helper-plugin-utils" "^7.0.0" "@babel/helper-regex" "^7.4.4" - regexpu-core "^4.5.4" + regexpu-core "^4.6.0" "@babel/plugin-syntax-async-generators@^7.2.0": version "7.2.0" @@ -391,10 +418,10 @@ dependencies: "@babel/helper-plugin-utils" "^7.0.0" -"@babel/plugin-transform-block-scoping@^7.5.5": - version "7.5.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.5.5.tgz#a35f395e5402822f10d2119f6f8e045e3639a2ce" - integrity sha512-82A3CLRRdYubkG85lKwhZB0WZoHxLGsJdux/cOVaJCJpvYFl1LVzAIFyRsa7CvXqW8rBM4Zf3Bfn8PHt5DP0Sg== +"@babel/plugin-transform-block-scoping@^7.6.0", "@babel/plugin-transform-block-scoping@^7.6.3": + version "7.6.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.6.3.tgz#6e854e51fbbaa84351b15d4ddafe342f3a5d542a" + integrity sha512-7hvrg75dubcO3ZI2rjYTzUrEuh1E9IyDEhhB6qfcooxhDA33xx2MasuLVgdxzcP6R/lipAC6n9ub9maNW6RKdw== dependencies: "@babel/helper-plugin-utils" "^7.0.0" lodash "^4.17.13" @@ -420,21 +447,21 @@ dependencies: "@babel/helper-plugin-utils" "^7.0.0" -"@babel/plugin-transform-destructuring@7.5.0", "@babel/plugin-transform-destructuring@^7.5.0": - version "7.5.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.5.0.tgz#f6c09fdfe3f94516ff074fe877db7bc9ef05855a" - integrity sha512-YbYgbd3TryYYLGyC7ZR+Tq8H/+bCmwoaxHfJHupom5ECstzbRLTch6gOQbhEY9Z4hiCNHEURgq06ykFv9JZ/QQ== +"@babel/plugin-transform-destructuring@7.6.0", "@babel/plugin-transform-destructuring@^7.6.0": + version "7.6.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.6.0.tgz#44bbe08b57f4480094d57d9ffbcd96d309075ba6" + integrity sha512-2bGIS5P1v4+sWTCnKNDZDxbGvEqi0ijeqM/YqHtVGrvG2y0ySgnEEhXErvE9dA0bnIzY9bIzdFK0jFA46ASIIQ== dependencies: "@babel/helper-plugin-utils" "^7.0.0" -"@babel/plugin-transform-dotall-regex@^7.4.4": - version "7.4.4" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.4.4.tgz#361a148bc951444312c69446d76ed1ea8e4450c3" - integrity sha512-P05YEhRc2h53lZDjRPk/OektxCVevFzZs2Gfjd545Wde3k+yFDbXORgl2e0xpbq8mLcKJ7Idss4fAg0zORN/zg== +"@babel/plugin-transform-dotall-regex@^7.4.4", "@babel/plugin-transform-dotall-regex@^7.6.2": + version "7.6.2" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.6.2.tgz#44abb948b88f0199a627024e1508acaf8dc9b2f9" + integrity sha512-KGKT9aqKV+9YMZSkowzYoYEiHqgaDhGmPNZlZxX6UeHC4z30nC1J9IrZuGqbYFB1jaIGdv91ujpze0exiVK8bA== dependencies: "@babel/helper-plugin-utils" "^7.0.0" "@babel/helper-regex" "^7.4.4" - regexpu-core "^4.5.4" + regexpu-core "^4.6.0" "@babel/plugin-transform-duplicate-keys@^7.5.0": version "7.5.0" @@ -497,10 +524,10 @@ "@babel/helper-plugin-utils" "^7.0.0" babel-plugin-dynamic-import-node "^2.3.0" -"@babel/plugin-transform-modules-commonjs@^7.5.0": - version "7.5.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.5.0.tgz#425127e6045231360858eeaa47a71d75eded7a74" - integrity sha512-xmHq0B+ytyrWJvQTc5OWAC4ii6Dhr0s22STOoydokG51JjWhyYo5mRPXoi+ZmtHQhZZwuXNN+GG5jy5UZZJxIQ== +"@babel/plugin-transform-modules-commonjs@^7.6.0": + version "7.6.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.6.0.tgz#39dfe957de4420445f1fcf88b68a2e4aa4515486" + integrity sha512-Ma93Ix95PNSEngqomy5LSBMAQvYKVe3dy+JlVJSHEXZR5ASL9lQBedMiCyVtmTLraIDVRE3ZjTZvmXXD2Ozw3g== dependencies: "@babel/helper-module-transforms" "^7.4.4" "@babel/helper-plugin-utils" "^7.0.0" @@ -524,12 +551,12 @@ "@babel/helper-module-transforms" "^7.1.0" "@babel/helper-plugin-utils" "^7.0.0" -"@babel/plugin-transform-named-capturing-groups-regex@^7.4.5": - version "7.4.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.4.5.tgz#9d269fd28a370258199b4294736813a60bbdd106" - integrity sha512-z7+2IsWafTBbjNsOxU/Iv5CvTJlr5w4+HGu1HovKYTtgJ362f7kBcQglkfmlspKKZ3bgrbSGvLfNx++ZJgCWsg== +"@babel/plugin-transform-named-capturing-groups-regex@^7.6.0", "@babel/plugin-transform-named-capturing-groups-regex@^7.6.3": + version "7.6.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.6.3.tgz#aaa6e409dd4fb2e50b6e2a91f7e3a3149dbce0cf" + integrity sha512-jTkk7/uE6H2s5w6VlMHeWuH+Pcy2lmdwFoeWCVnvIrDUnB5gQqTVI8WfmEAhF2CDEarGrknZcmSFg1+bkfCoSw== dependencies: - regexp-tree "^0.1.6" + regexpu-core "^4.6.0" "@babel/plugin-transform-new-target@^7.4.4": version "7.4.4" @@ -563,9 +590,9 @@ "@babel/helper-plugin-utils" "^7.0.0" "@babel/plugin-transform-react-constant-elements@^7.0.0": - version "7.5.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-constant-elements/-/plugin-transform-react-constant-elements-7.5.0.tgz#4d6ae4033bc38f8a65dfca2b6235c44522a422fc" - integrity sha512-c5Ba8cpybZFp1Izkf2sWGuNjOxoQ32tFgBvvYvwGhi4+9f6vGiSK9Gex4uVuO/Va6YJFu41aAh1MzMjUWkp0IQ== + version "7.6.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-constant-elements/-/plugin-transform-react-constant-elements-7.6.3.tgz#9fc9ea060b983c7c035acbe481cbe1fb1245bfff" + integrity sha512-1/YogSSU7Tby9rq2VCmhuRg+6pxsHy2rI7w/oo8RKoBt6uBUFG+mk6x13kK+FY1/ggN92HAfg7ADd1v1+NCOKg== dependencies: "@babel/helper-annotate-as-pure" "^7.0.0" "@babel/helper-plugin-utils" "^7.0.0" @@ -616,10 +643,10 @@ dependencies: "@babel/helper-plugin-utils" "^7.0.0" -"@babel/plugin-transform-runtime@7.5.5": - version "7.5.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.5.5.tgz#a6331afbfc59189d2135b2e09474457a8e3d28bc" - integrity sha512-6Xmeidsun5rkwnGfMOp6/z9nSzWpHFNVr2Jx7kwoq4mVatQfQx5S56drBgEHF+XQbKOdIaOiMIINvp/kAwMN+w== +"@babel/plugin-transform-runtime@7.6.0": + version "7.6.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.6.0.tgz#85a3cce402b28586138e368fce20ab3019b9713e" + integrity sha512-Da8tMf7uClzwUm/pnJ1S93m/aRXmoYNDD7TkHua8xBDdaAs54uZpTWvEt6NGwmoVMb9mZbntfTqmG2oSzN/7Vg== dependencies: "@babel/helper-module-imports" "^7.0.0" "@babel/helper-plugin-utils" "^7.0.0" @@ -633,10 +660,10 @@ dependencies: "@babel/helper-plugin-utils" "^7.0.0" -"@babel/plugin-transform-spread@^7.2.0": - version "7.2.2" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-spread/-/plugin-transform-spread-7.2.2.tgz#3103a9abe22f742b6d406ecd3cd49b774919b406" - integrity sha512-KWfky/58vubwtS0hLqEnrWJjsMGaOeSBn90Ezn5Jeg9Z8KKHmELbP1yGylMlm5N6TPKeY9A2+UaSYLdxahg01w== +"@babel/plugin-transform-spread@^7.2.0", "@babel/plugin-transform-spread@^7.6.2": + version "7.6.2" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-spread/-/plugin-transform-spread-7.6.2.tgz#fc77cf798b24b10c46e1b51b1b88c2bf661bb8dd" + integrity sha512-DpSvPFryKdK1x+EDJYCy28nmAaIMdxmhot62jAXF/o99iA33Zj2Lmcp3vDmz+MUh0LNYVPvfj5iC3feb3/+PFg== dependencies: "@babel/helper-plugin-utils" "^7.0.0" @@ -663,28 +690,28 @@ dependencies: "@babel/helper-plugin-utils" "^7.0.0" -"@babel/plugin-transform-typescript@^7.3.2": - version "7.5.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.5.5.tgz#6d862766f09b2da1cb1f7d505fe2aedab6b7d4b8" - integrity sha512-pehKf4m640myZu5B2ZviLaiBlxMCjSZ1qTEO459AXKX5GnPueyulJeCqZFs1nz/Ya2dDzXQ1NxZ/kKNWyD4h6w== +"@babel/plugin-transform-typescript@^7.6.0": + version "7.6.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.6.3.tgz#dddb50cf3b8b2ef70b22e5326e9a91f05a1db13b" + integrity sha512-aiWINBrPMSC3xTXRNM/dfmyYuPNKY/aexYqBgh0HBI5Y+WO5oRAqW/oROYeYHrF4Zw12r9rK4fMk/ZlAmqx/FQ== dependencies: - "@babel/helper-create-class-features-plugin" "^7.5.5" + "@babel/helper-create-class-features-plugin" "^7.6.0" "@babel/helper-plugin-utils" "^7.0.0" "@babel/plugin-syntax-typescript" "^7.2.0" -"@babel/plugin-transform-unicode-regex@^7.4.4": - version "7.4.4" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.4.4.tgz#ab4634bb4f14d36728bf5978322b35587787970f" - integrity sha512-il+/XdNw01i93+M9J9u4T7/e/Ue/vWfNZE4IRUQjplu2Mqb/AFTDimkw2tdEdSH50wuQXZAbXSql0UphQke+vA== +"@babel/plugin-transform-unicode-regex@^7.4.4", "@babel/plugin-transform-unicode-regex@^7.6.2": + version "7.6.2" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.6.2.tgz#b692aad888a7e8d8b1b214be6b9dc03d5031f698" + integrity sha512-orZI6cWlR3nk2YmYdb0gImrgCUwb5cBUwjf6Ks6dvNVvXERkwtJWOQaEOjPiu0Gu1Tq6Yq/hruCZZOOi9F34Dw== dependencies: "@babel/helper-plugin-utils" "^7.0.0" "@babel/helper-regex" "^7.4.4" - regexpu-core "^4.5.4" + regexpu-core "^4.6.0" -"@babel/preset-env@7.5.5", "@babel/preset-env@^7.4.5": - version "7.5.5" - resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.5.5.tgz#bc470b53acaa48df4b8db24a570d6da1fef53c9a" - integrity sha512-GMZQka/+INwsMz1A5UEql8tG015h5j/qjptpKY2gJ7giy8ohzU710YciJB5rcKsWGWHiW3RUnHib0E5/m3Tp3A== +"@babel/preset-env@7.6.0": + version "7.6.0" + resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.6.0.tgz#aae4141c506100bb2bfaa4ac2a5c12b395619e50" + integrity sha512-1efzxFv/TcPsNXlRhMzRnkBFMeIqBBgzwmZwlFDw5Ubj0AGLeufxugirwZmkkX/ayi3owsSqoQ4fw8LkfK9SYg== dependencies: "@babel/helper-module-imports" "^7.0.0" "@babel/helper-plugin-utils" "^7.0.0" @@ -702,10 +729,10 @@ "@babel/plugin-transform-arrow-functions" "^7.2.0" "@babel/plugin-transform-async-to-generator" "^7.5.0" "@babel/plugin-transform-block-scoped-functions" "^7.2.0" - "@babel/plugin-transform-block-scoping" "^7.5.5" + "@babel/plugin-transform-block-scoping" "^7.6.0" "@babel/plugin-transform-classes" "^7.5.5" "@babel/plugin-transform-computed-properties" "^7.2.0" - "@babel/plugin-transform-destructuring" "^7.5.0" + "@babel/plugin-transform-destructuring" "^7.6.0" "@babel/plugin-transform-dotall-regex" "^7.4.4" "@babel/plugin-transform-duplicate-keys" "^7.5.0" "@babel/plugin-transform-exponentiation-operator" "^7.2.0" @@ -714,10 +741,10 @@ "@babel/plugin-transform-literals" "^7.2.0" "@babel/plugin-transform-member-expression-literals" "^7.2.0" "@babel/plugin-transform-modules-amd" "^7.5.0" - "@babel/plugin-transform-modules-commonjs" "^7.5.0" + "@babel/plugin-transform-modules-commonjs" "^7.6.0" "@babel/plugin-transform-modules-systemjs" "^7.5.0" "@babel/plugin-transform-modules-umd" "^7.2.0" - "@babel/plugin-transform-named-capturing-groups-regex" "^7.4.5" + "@babel/plugin-transform-named-capturing-groups-regex" "^7.6.0" "@babel/plugin-transform-new-target" "^7.4.4" "@babel/plugin-transform-object-super" "^7.5.5" "@babel/plugin-transform-parameters" "^7.4.4" @@ -730,14 +757,70 @@ "@babel/plugin-transform-template-literals" "^7.4.4" "@babel/plugin-transform-typeof-symbol" "^7.2.0" "@babel/plugin-transform-unicode-regex" "^7.4.4" - "@babel/types" "^7.5.5" + "@babel/types" "^7.6.0" browserslist "^4.6.0" core-js-compat "^3.1.1" invariant "^2.2.2" js-levenshtein "^1.1.3" semver "^5.5.0" -"@babel/preset-react@7.0.0", "@babel/preset-react@^7.0.0": +"@babel/preset-env@^7.4.5": + version "7.6.3" + resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.6.3.tgz#9e1bf05a2e2d687036d24c40e4639dc46cef2271" + integrity sha512-CWQkn7EVnwzlOdR5NOm2+pfgSNEZmvGjOhlCHBDq0J8/EStr+G+FvPEiz9B56dR6MoiUFjXhfE4hjLoAKKJtIQ== + dependencies: + "@babel/helper-module-imports" "^7.0.0" + "@babel/helper-plugin-utils" "^7.0.0" + "@babel/plugin-proposal-async-generator-functions" "^7.2.0" + "@babel/plugin-proposal-dynamic-import" "^7.5.0" + "@babel/plugin-proposal-json-strings" "^7.2.0" + "@babel/plugin-proposal-object-rest-spread" "^7.6.2" + "@babel/plugin-proposal-optional-catch-binding" "^7.2.0" + "@babel/plugin-proposal-unicode-property-regex" "^7.6.2" + "@babel/plugin-syntax-async-generators" "^7.2.0" + "@babel/plugin-syntax-dynamic-import" "^7.2.0" + "@babel/plugin-syntax-json-strings" "^7.2.0" + "@babel/plugin-syntax-object-rest-spread" "^7.2.0" + "@babel/plugin-syntax-optional-catch-binding" "^7.2.0" + "@babel/plugin-transform-arrow-functions" "^7.2.0" + "@babel/plugin-transform-async-to-generator" "^7.5.0" + "@babel/plugin-transform-block-scoped-functions" "^7.2.0" + "@babel/plugin-transform-block-scoping" "^7.6.3" + "@babel/plugin-transform-classes" "^7.5.5" + "@babel/plugin-transform-computed-properties" "^7.2.0" + "@babel/plugin-transform-destructuring" "^7.6.0" + "@babel/plugin-transform-dotall-regex" "^7.6.2" + "@babel/plugin-transform-duplicate-keys" "^7.5.0" + "@babel/plugin-transform-exponentiation-operator" "^7.2.0" + "@babel/plugin-transform-for-of" "^7.4.4" + "@babel/plugin-transform-function-name" "^7.4.4" + "@babel/plugin-transform-literals" "^7.2.0" + "@babel/plugin-transform-member-expression-literals" "^7.2.0" + "@babel/plugin-transform-modules-amd" "^7.5.0" + "@babel/plugin-transform-modules-commonjs" "^7.6.0" + "@babel/plugin-transform-modules-systemjs" "^7.5.0" + "@babel/plugin-transform-modules-umd" "^7.2.0" + "@babel/plugin-transform-named-capturing-groups-regex" "^7.6.3" + "@babel/plugin-transform-new-target" "^7.4.4" + "@babel/plugin-transform-object-super" "^7.5.5" + "@babel/plugin-transform-parameters" "^7.4.4" + "@babel/plugin-transform-property-literals" "^7.2.0" + "@babel/plugin-transform-regenerator" "^7.4.5" + "@babel/plugin-transform-reserved-words" "^7.2.0" + "@babel/plugin-transform-shorthand-properties" "^7.2.0" + "@babel/plugin-transform-spread" "^7.6.2" + "@babel/plugin-transform-sticky-regex" "^7.2.0" + "@babel/plugin-transform-template-literals" "^7.4.4" + "@babel/plugin-transform-typeof-symbol" "^7.2.0" + "@babel/plugin-transform-unicode-regex" "^7.6.2" + "@babel/types" "^7.6.3" + browserslist "^4.6.0" + core-js-compat "^3.1.1" + invariant "^2.2.2" + js-levenshtein "^1.1.3" + semver "^5.5.0" + +"@babel/preset-react@7.0.0": version "7.0.0" resolved "https://registry.yarnpkg.com/@babel/preset-react/-/preset-react-7.0.0.tgz#e86b4b3d99433c7b3e9e91747e2653958bc6b3c0" integrity sha512-oayxyPS4Zj+hF6Et11BwuBkmpgT/zMxyuZgFrMeZID6Hdh3dGlk4sHCAhdBCpuCKW2ppBfl2uCCetlrUIJRY3w== @@ -748,49 +831,67 @@ "@babel/plugin-transform-react-jsx-self" "^7.0.0" "@babel/plugin-transform-react-jsx-source" "^7.0.0" -"@babel/preset-typescript@7.3.3": - version "7.3.3" - resolved "https://registry.yarnpkg.com/@babel/preset-typescript/-/preset-typescript-7.3.3.tgz#88669911053fa16b2b276ea2ede2ca603b3f307a" - integrity sha512-mzMVuIP4lqtn4du2ynEfdO0+RYcslwrZiJHXu4MGaC1ctJiW2fyaeDrtjJGs7R/KebZ1sgowcIoWf4uRpEfKEg== +"@babel/preset-react@^7.0.0": + version "7.6.3" + resolved "https://registry.yarnpkg.com/@babel/preset-react/-/preset-react-7.6.3.tgz#d5242c828322520205ae4eda5d4f4f618964e2f6" + integrity sha512-07yQhmkZmRAfwREYIQgW0HEwMY9GBJVuPY4Q12UC72AbfaawuupVWa8zQs2tlL+yun45Nv/1KreII/0PLfEsgA== dependencies: "@babel/helper-plugin-utils" "^7.0.0" - "@babel/plugin-transform-typescript" "^7.3.2" + "@babel/plugin-transform-react-display-name" "^7.0.0" + "@babel/plugin-transform-react-jsx" "^7.0.0" + "@babel/plugin-transform-react-jsx-self" "^7.0.0" + "@babel/plugin-transform-react-jsx-source" "^7.0.0" -"@babel/runtime@7.5.5", "@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2", "@babel/runtime@^7.2.0", "@babel/runtime@^7.3.4", "@babel/runtime@^7.4.0", "@babel/runtime@^7.4.2", "@babel/runtime@^7.4.5", "@babel/runtime@^7.5.5": - version "7.5.5" - resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.5.5.tgz#74fba56d35efbeca444091c7850ccd494fd2f132" - integrity sha512-28QvEGyQyNkB0/m2B4FU7IEZGK2NUrcMtT6BZEFALTguLk+AUT6ofsHtPk5QyjAdUkpMJ+/Em+quwz4HOt30AQ== +"@babel/preset-typescript@7.6.0": + version "7.6.0" + resolved "https://registry.yarnpkg.com/@babel/preset-typescript/-/preset-typescript-7.6.0.tgz#25768cb8830280baf47c45ab1a519a9977498c98" + integrity sha512-4xKw3tTcCm0qApyT6PqM9qniseCE79xGHiUnNdKGdxNsGUc2X7WwZybqIpnTmoukg3nhPceI5KPNzNqLNeIJww== + dependencies: + "@babel/helper-plugin-utils" "^7.0.0" + "@babel/plugin-transform-typescript" "^7.6.0" + +"@babel/runtime@7.6.0": + version "7.6.0" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.6.0.tgz#4fc1d642a9fd0299754e8b5de62c631cf5568205" + integrity sha512-89eSBLJsxNxOERC0Op4vd+0Bqm6wRMqMbFtV3i0/fbaWw/mJ8Q3eBvgX0G4SyrOOLCtbu98HspF8o09MRT+KzQ== dependencies: regenerator-runtime "^0.13.2" -"@babel/template@^7.1.0", "@babel/template@^7.4.0", "@babel/template@^7.4.4": - version "7.4.4" - resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.4.4.tgz#f4b88d1225689a08f5bc3a17483545be9e4ed237" - integrity sha512-CiGzLN9KgAvgZsnivND7rkA+AeJ9JB0ciPOD4U59GKbQP2iQl+olF1l76kJOupqidozfZ32ghwBEJDhnk9MEcw== +"@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2", "@babel/runtime@^7.2.0", "@babel/runtime@^7.3.4", "@babel/runtime@^7.4.0", "@babel/runtime@^7.4.2", "@babel/runtime@^7.4.5", "@babel/runtime@^7.5.5": + version "7.6.3" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.6.3.tgz#935122c74c73d2240cafd32ddb5fc2a6cd35cf1f" + integrity sha512-kq6anf9JGjW8Nt5rYfEuGRaEAaH1mkv3Bbu6rYvLOpPh/RusSJXuKPEAoZ7L7gybZkchE8+NV5g9vKF4AGAtsA== + dependencies: + regenerator-runtime "^0.13.2" + +"@babel/template@^7.1.0", "@babel/template@^7.4.0", "@babel/template@^7.4.4", "@babel/template@^7.6.0": + version "7.6.0" + resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.6.0.tgz#7f0159c7f5012230dad64cca42ec9bdb5c9536e6" + integrity sha512-5AEH2EXD8euCk446b7edmgFdub/qfH1SN6Nii3+fyXP807QRx9Q73A2N5hNwRRslC2H9sNzaFhsPubkS4L8oNQ== dependencies: "@babel/code-frame" "^7.0.0" - "@babel/parser" "^7.4.4" - "@babel/types" "^7.4.4" + "@babel/parser" "^7.6.0" + "@babel/types" "^7.6.0" -"@babel/traverse@^7.0.0", "@babel/traverse@^7.1.0", "@babel/traverse@^7.4.3", "@babel/traverse@^7.4.4", "@babel/traverse@^7.5.5": - version "7.5.5" - resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.5.5.tgz#f664f8f368ed32988cd648da9f72d5ca70f165bb" - integrity sha512-MqB0782whsfffYfSjH4TM+LMjrJnhCNEDMDIjeTpl+ASaUvxcjoiVCo/sM1GhS1pHOXYfWVCYneLjMckuUxDaQ== +"@babel/traverse@^7.0.0", "@babel/traverse@^7.1.0", "@babel/traverse@^7.4.3", "@babel/traverse@^7.4.4", "@babel/traverse@^7.5.5", "@babel/traverse@^7.6.0", "@babel/traverse@^7.6.2", "@babel/traverse@^7.6.3": + version "7.6.3" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.6.3.tgz#66d7dba146b086703c0fb10dd588b7364cec47f9" + integrity sha512-unn7P4LGsijIxaAJo/wpoU11zN+2IaClkQAxcJWBNCMS6cmVh802IyLHNkAjQ0iYnRS3nnxk5O3fuXW28IMxTw== dependencies: "@babel/code-frame" "^7.5.5" - "@babel/generator" "^7.5.5" + "@babel/generator" "^7.6.3" "@babel/helper-function-name" "^7.1.0" "@babel/helper-split-export-declaration" "^7.4.4" - "@babel/parser" "^7.5.5" - "@babel/types" "^7.5.5" + "@babel/parser" "^7.6.3" + "@babel/types" "^7.6.3" debug "^4.1.0" globals "^11.1.0" lodash "^4.17.13" -"@babel/types@^7.0.0", "@babel/types@^7.2.0", "@babel/types@^7.3.0", "@babel/types@^7.4.0", "@babel/types@^7.4.4", "@babel/types@^7.5.5": - version "7.5.5" - resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.5.5.tgz#97b9f728e182785909aa4ab56264f090a028d18a" - integrity sha512-s63F9nJioLqOlW3UkyMd+BYhXt44YuaFm/VV0VwuteqjYwRrObkU7ra9pY4wAJR3oXi8hJrMcrcJdO/HH33vtw== +"@babel/types@^7.0.0", "@babel/types@^7.2.0", "@babel/types@^7.3.0", "@babel/types@^7.4.0", "@babel/types@^7.4.4", "@babel/types@^7.5.5", "@babel/types@^7.6.0", "@babel/types@^7.6.3": + version "7.6.3" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.6.3.tgz#3f07d96f854f98e2fbd45c64b0cb942d11e8ba09" + integrity sha512-CqbcpTxMcpuQTMhjI37ZHVgjBkysg5icREQIEZ0eG1yCNwg3oy+5AaLiOKmjsCj6nqOsa6Hf0ObjRVwokb7srA== dependencies: esutils "^2.0.2" lodash "^4.17.13" @@ -814,47 +915,46 @@ resolved "https://registry.yarnpkg.com/@csstools/normalize.css/-/normalize.css-9.0.1.tgz#c27b391d8457d1e893f1eddeaf5e5412d12ffbb5" integrity sha512-6It2EVfGskxZCQhuykrfnALg7oVeiI6KclWSmGDqB0AiInVrTGB9Jp9i4/Ad21u9Jde/voVQz6eFX/eSg/UsPA== -"@fortawesome/fontawesome-common-types@^0.2.22": - version "0.2.22" - resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-0.2.22.tgz#3f1328d232a0fd5de8484d833c8519426f39f016" - integrity sha512-QmEuZsipX5/cR9JOg0fsTN4Yr/9lieYWM8AQpmRa0eIfeOcl/HLYoEa366BCGRSrgNJEexuvOgbq9jnJ22IY5g== +"@fortawesome/fontawesome-common-types@^0.2.25": + version "0.2.25" + resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-0.2.25.tgz#6df015905081f2762e5cfddeb7a20d2e9b16c786" + integrity sha512-3RuZPDuuPELd7RXtUqTCfed14fcny9UiPOkdr2i+cYxBoTOfQgxcDoq77fHiiHcgWuo1LoBUpvGxFF1H/y7s3Q== -"@fortawesome/fontawesome-svg-core@^1.2.19": - version "1.2.22" - resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-1.2.22.tgz#9a6117c96c8b823c7d531000568ac75c3c02e123" - integrity sha512-Q941E4x8UfnMH3308n0qrgoja+GoqyiV846JTLoCcCWAKokLKrixCkq6RDBs8r+TtAWaLUrBpI+JFxQNX/WNPQ== +"@fortawesome/fontawesome-svg-core@^1.2.25": + version "1.2.25" + resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-1.2.25.tgz#24b03391d14f0c6171e8cad7057c687b74049790" + integrity sha512-MotKnn53JKqbkLQiwcZSBJVYtTgIKFbh7B8+kd05TSnfKYPFmjKKI59o2fpz5t0Hzl35vVGU6+N4twoOpZUrqA== dependencies: - "@fortawesome/fontawesome-common-types" "^0.2.22" + "@fortawesome/fontawesome-common-types" "^0.2.25" -"@fortawesome/free-solid-svg-icons@^5.9.0": - version "5.10.2" - resolved "https://registry.yarnpkg.com/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-5.10.2.tgz#61bcecce3aa5001fd154826238dfa840de4aa05a" - integrity sha512-9Os/GRUcy+iVaznlg8GKcPSQFpIQpAg14jF0DWsMdnpJfIftlvfaQCWniR/ex9FoOpSEOrlXqmUCFL+JGeciuA== +"@fortawesome/free-solid-svg-icons@^5.11.2": + version "5.11.2" + resolved "https://registry.yarnpkg.com/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-5.11.2.tgz#2f2f1459743a27902b76655a0d0bc5ec4d945631" + integrity sha512-zBue4i0PAZJUXOmLBBvM7L0O7wmsDC8dFv9IhpW5QL4kT9xhhVUsYg/LX1+5KaukWq4/cbDcKT+RT1aRe543sg== dependencies: - "@fortawesome/fontawesome-common-types" "^0.2.22" + "@fortawesome/fontawesome-common-types" "^0.2.25" -"@fortawesome/react-fontawesome@^0.1.4": - version "0.1.4" - resolved "https://registry.yarnpkg.com/@fortawesome/react-fontawesome/-/react-fontawesome-0.1.4.tgz#18d61d9b583ca289a61aa7dccc05bd164d6bc9ad" - integrity sha512-GwmxQ+TK7PEdfSwvxtGnMCqrfEm0/HbRHArbUudsYiy9KzVCwndxa2KMcfyTQ8El0vROrq8gOOff09RF1oQe8g== +"@fortawesome/react-fontawesome@^0.1.7": + version "0.1.7" + resolved "https://registry.yarnpkg.com/@fortawesome/react-fontawesome/-/react-fontawesome-0.1.7.tgz#c004ca75c15c5a1218101e8f042b8da8dec0c4b5" + integrity sha512-AHWSzOsHBe5vqOkrvs+CKw+8eLl+0XZsVixOWhTPpGpOA8WQUbVU6J9cmtAvTaxUU5OIf+rgxxF8ZKc3BVldxg== dependencies: - humps "^2.0.1" prop-types "^15.5.10" "@hapi/address@2.x.x": - version "2.0.0" - resolved "https://registry.yarnpkg.com/@hapi/address/-/address-2.0.0.tgz#9f05469c88cb2fd3dcd624776b54ee95c312126a" - integrity sha512-mV6T0IYqb0xL1UALPFplXYQmR0twnXG0M6jUswpquqT2sD12BOiCiLy3EvMp/Fy7s3DZElC4/aPjEjo2jeZpvw== + version "2.1.2" + resolved "https://registry.yarnpkg.com/@hapi/address/-/address-2.1.2.tgz#1c794cd6dbf2354d1eb1ef10e0303f573e1c7222" + integrity sha512-O4QDrx+JoGKZc6aN64L04vqa7e41tIiLU+OvKdcYaEMP97UttL0f9GIi9/0A4WAMx0uBd6SidDIhktZhgOcN8Q== "@hapi/bourne@1.x.x": version "1.3.2" resolved "https://registry.yarnpkg.com/@hapi/bourne/-/bourne-1.3.2.tgz#0a7095adea067243ce3283e1b56b8a8f453b242a" integrity sha512-1dVNHT76Uu5N3eJNTYcvxee+jzX4Z9lfciqRRHCU27ihbUcYi+iSc2iml5Ke1LXe1SyJCLA0+14Jh4tXJgOppA== -"@hapi/hoek@8.x.x": - version "8.2.1" - resolved "https://registry.yarnpkg.com/@hapi/hoek/-/hoek-8.2.1.tgz#924af04cbb22e17359c620d2a9c946e63f58eb77" - integrity sha512-JPiBy+oSmsq3St7XlipfN5pNA6bDJ1kpa73PrK/zR29CVClDVqy04AanM/M/qx5bSF+I61DdCfAvRrujau+zRg== +"@hapi/hoek@8.x.x", "@hapi/hoek@^8.3.0": + version "8.3.2" + resolved "https://registry.yarnpkg.com/@hapi/hoek/-/hoek-8.3.2.tgz#91e7188edebc5d876f0b91a860f555ff06f0782b" + integrity sha512-NP5SG4bzix+EtSMtcudp8TvI0lB46mXNo8uFpTDw6tqxGx4z5yx+giIunEFA0Z7oUO4DuWrOJV9xqR2tJVEdyA== "@hapi/joi@^15.0.0": version "15.1.1" @@ -867,11 +967,11 @@ "@hapi/topo" "3.x.x" "@hapi/topo@3.x.x": - version "3.1.3" - resolved "https://registry.yarnpkg.com/@hapi/topo/-/topo-3.1.3.tgz#c7a02e0d936596d29f184e6d7fdc07e8b5efce11" - integrity sha512-JmS9/vQK6dcUYn7wc2YZTqzIKubAQcJKu2KCKAru6es482U5RT5fP1EXCPtlXpiK7PR0On/kpQKI4fRKkzpZBQ== + version "3.1.6" + resolved "https://registry.yarnpkg.com/@hapi/topo/-/topo-3.1.6.tgz#68d935fa3eae7fdd5ab0d7f953f3205d8b2bfc29" + integrity sha512-tAag0jEcjwH+P2quUfipd7liWCNX2F8NvYjQp2wtInsZxnMlypdw0FtAOLxtvvkO+GSRRbmNi8m/5y42PQJYCQ== dependencies: - "@hapi/hoek" "8.x.x" + "@hapi/hoek" "^8.3.0" "@jest/console@^24.7.1", "@jest/console@^24.9.0": version "24.9.0" @@ -1012,7 +1112,7 @@ source-map "^0.6.1" write-file-atomic "2.4.1" -"@jest/types@^24.8.0", "@jest/types@^24.9.0": +"@jest/types@^24.9.0": version "24.9.0" resolved "https://registry.yarnpkg.com/@jest/types/-/types-24.9.0.tgz#63cb26cb7500d069e5a389441a7c6ab5e909fc59" integrity sha512-XKK7ze1apu5JWQ5eZjHITP66AX+QsLlbaJRBGYr8pNzwcAE2JVkwnf0yqjHTsDRcjR0mujy/NmZMXw5kl+kGBw== @@ -1054,10 +1154,10 @@ resolved "https://registry.yarnpkg.com/@svgr/babel-plugin-replace-jsx-attribute-value/-/babel-plugin-replace-jsx-attribute-value-4.2.0.tgz#310ec0775de808a6a2e4fd4268c245fd734c1165" integrity sha512-U9m870Kqm0ko8beHawRXLGLvSi/ZMrl89gJ5BNcT452fAjtF2p4uRzXkdzvGJJJYBgx7BmqlDjBN/eCp5AAX2w== -"@svgr/babel-plugin-svg-dynamic-title@^4.3.1": - version "4.3.1" - resolved "https://registry.yarnpkg.com/@svgr/babel-plugin-svg-dynamic-title/-/babel-plugin-svg-dynamic-title-4.3.1.tgz#646c2f5b5770c2fe318d6e51492344c3d62ddb63" - integrity sha512-p6z6JJroP989jHWcuraeWpzdejehTmLUpyC9smhTBWyPN0VVGe2phbYxpPTV7Vh8XzmFrcG55idrnfWn/2oQEw== +"@svgr/babel-plugin-svg-dynamic-title@^4.3.3": + version "4.3.3" + resolved "https://registry.yarnpkg.com/@svgr/babel-plugin-svg-dynamic-title/-/babel-plugin-svg-dynamic-title-4.3.3.tgz#2cdedd747e5b1b29ed4c241e46256aac8110dd93" + integrity sha512-w3Be6xUNdwgParsvxkkeZb545VhXEwjGMwExMVBIdPQJeyMQHqm9Msnb2a1teHBqUYL66qtwfhNkbj1iarCG7w== "@svgr/babel-plugin-svg-em-dimensions@^4.2.0": version "4.2.0" @@ -1074,26 +1174,26 @@ resolved "https://registry.yarnpkg.com/@svgr/babel-plugin-transform-svg-component/-/babel-plugin-transform-svg-component-4.2.0.tgz#5f1e2f886b2c85c67e76da42f0f6be1b1767b697" integrity sha512-hYfYuZhQPCBVotABsXKSCfel2slf/yvJY8heTVX1PCTaq/IgASq1IyxPPKJ0chWREEKewIU/JMSsIGBtK1KKxw== -"@svgr/babel-preset@^4.3.1": - version "4.3.1" - resolved "https://registry.yarnpkg.com/@svgr/babel-preset/-/babel-preset-4.3.1.tgz#62ffcb85d756580e8ce608e9d2ac3b9063be9e28" - integrity sha512-rPFKLmyhlh6oeBv3j2vEAj2nd2QbWqpoJLKzBLjwQVt+d9aeXajVaPNEqrES2spjXKR4OxfgSs7U0NtmAEkr0Q== +"@svgr/babel-preset@^4.3.3": + version "4.3.3" + resolved "https://registry.yarnpkg.com/@svgr/babel-preset/-/babel-preset-4.3.3.tgz#a75d8c2f202ac0e5774e6bfc165d028b39a1316c" + integrity sha512-6PG80tdz4eAlYUN3g5GZiUjg2FMcp+Wn6rtnz5WJG9ITGEF1pmFdzq02597Hn0OmnQuCVaBYQE1OVFAnwOl+0A== dependencies: "@svgr/babel-plugin-add-jsx-attribute" "^4.2.0" "@svgr/babel-plugin-remove-jsx-attribute" "^4.2.0" "@svgr/babel-plugin-remove-jsx-empty-expression" "^4.2.0" "@svgr/babel-plugin-replace-jsx-attribute-value" "^4.2.0" - "@svgr/babel-plugin-svg-dynamic-title" "^4.3.1" + "@svgr/babel-plugin-svg-dynamic-title" "^4.3.3" "@svgr/babel-plugin-svg-em-dimensions" "^4.2.0" "@svgr/babel-plugin-transform-react-native-svg" "^4.2.0" "@svgr/babel-plugin-transform-svg-component" "^4.2.0" "@svgr/core@^4.3.2": - version "4.3.2" - resolved "https://registry.yarnpkg.com/@svgr/core/-/core-4.3.2.tgz#939c89be670ad79b762f4c063f213f0e02535f2e" - integrity sha512-N+tP5CLFd1hP9RpO83QJPZY3NL8AtrdqNbuhRgBkjE/49RnMrrRsFm1wY8pueUfAGvzn6tSXUq29o6ah8RuR5w== + version "4.3.3" + resolved "https://registry.yarnpkg.com/@svgr/core/-/core-4.3.3.tgz#b37b89d5b757dc66e8c74156d00c368338d24293" + integrity sha512-qNuGF1QON1626UCaZamWt5yedpgOytvLj5BQZe2j1k1B8DUG4OyugZyfEwBeXozCUwhLEpsrgPrE+eCu4fY17w== dependencies: - "@svgr/plugin-jsx" "^4.3.2" + "@svgr/plugin-jsx" "^4.3.3" camelcase "^5.3.1" cosmiconfig "^5.2.1" @@ -1104,13 +1204,13 @@ dependencies: "@babel/types" "^7.4.4" -"@svgr/plugin-jsx@^4.3.2": - version "4.3.2" - resolved "https://registry.yarnpkg.com/@svgr/plugin-jsx/-/plugin-jsx-4.3.2.tgz#ce9ddafc8cdd74da884c9f7af014afcf37f93d3c" - integrity sha512-+1GW32RvmNmCsOkMoclA/TppNjHPLMnNZG3/Ecscxawp051XJ2MkO09Hn11VcotdC2EPrDfT8pELGRo+kbZ1Eg== +"@svgr/plugin-jsx@^4.3.2", "@svgr/plugin-jsx@^4.3.3": + version "4.3.3" + resolved "https://registry.yarnpkg.com/@svgr/plugin-jsx/-/plugin-jsx-4.3.3.tgz#e2ba913dbdfbe85252a34db101abc7ebd50992fa" + integrity sha512-cLOCSpNWQnDB1/v+SUENHH7a0XY09bfuMKdq9+gYvtuwzC2rU4I0wKGFEp1i24holdQdwodCtDQdFtJiTCWc+w== dependencies: "@babel/core" "^7.4.5" - "@svgr/babel-preset" "^4.3.1" + "@svgr/babel-preset" "^4.3.3" "@svgr/hast-util-to-babel-ast" "^4.3.2" svg-parser "^2.0.0" @@ -1138,9 +1238,9 @@ loader-utils "^1.2.3" "@types/babel__core@^7.1.0": - version "7.1.2" - resolved "https://registry.yarnpkg.com/@types/babel__core/-/babel__core-7.1.2.tgz#608c74f55928033fce18b99b213c16be4b3d114f" - integrity sha512-cfCCrFmiGY/yq0NuKNxIQvZFy9kY/1immpSpTngOnyIbD4+eJOG5mxphhHDv3CHL9GltO4GcKr54kGBg3RNdbg== + version "7.1.3" + resolved "https://registry.yarnpkg.com/@types/babel__core/-/babel__core-7.1.3.tgz#e441ea7df63cd080dfcd02ab199e6d16a735fc30" + integrity sha512-8fBo0UR2CcwWxeX7WIIgJ7lXjasFxoYgRnFHUj+hRvKkpiBJbxhdAPTCY6/ZKM0uxANFVzt4yObSLuTiTnazDA== dependencies: "@babel/parser" "^7.1.0" "@babel/types" "^7.0.0" @@ -1149,9 +1249,9 @@ "@types/babel__traverse" "*" "@types/babel__generator@*": - version "7.0.2" - resolved "https://registry.yarnpkg.com/@types/babel__generator/-/babel__generator-7.0.2.tgz#d2112a6b21fad600d7674274293c85dce0cb47fc" - integrity sha512-NHcOfab3Zw4q5sEE2COkpfXjoE7o+PmqD9DQW4koUT3roNxwziUdXGnRndMat/LJNUtePwn1TlP4do3uoe3KZQ== + version "7.6.0" + resolved "https://registry.yarnpkg.com/@types/babel__generator/-/babel__generator-7.6.0.tgz#f1ec1c104d1bb463556ecb724018ab788d0c172a" + integrity sha512-c1mZUu4up5cp9KROs/QAw0gTeHrw/x7m52LcnvMxxOZ03DmLwPV0MlGmlgzV3cnSdjhJOZsj7E7FHeioai+egw== dependencies: "@babel/types" "^7.0.0" @@ -1201,9 +1301,9 @@ integrity sha512-Il2DtDVRGDcqjDtE+rF8iqg1CArehSK84HZJCT7AMITlyXRBpuPhqGLDQMowraqqu1coEaimg4ZOqggt6L6L+A== "@types/node@*": - version "12.7.2" - resolved "https://registry.yarnpkg.com/@types/node/-/node-12.7.2.tgz#c4e63af5e8823ce9cc3f0b34f7b998c2171f0c44" - integrity sha512-dyYO+f6ihZEtNPDcWNR1fkoTDf3zAK3lAABDze3mz6POyIercH0lEUawUFXlG8xaQZmm1yEBON/4TsYv/laDYg== + version "12.11.1" + resolved "https://registry.yarnpkg.com/@types/node/-/node-12.11.1.tgz#1fd7b821f798b7fa29f667a1be8f3442bb8922a3" + integrity sha512-TJtwsqZ39pqcljJpajeoofYRfeZ7/I/OMUQ5pR4q5wOKf2ocrUvBAZUMhWsOvKx3dVc/aaV5GluBivt0sWqA5A== "@types/q@^1.5.1": version "1.5.2" @@ -1216,54 +1316,57 @@ integrity sha512-l42BggppR6zLmpfU6fq9HEa2oGPEI8yrSPL3GITjfRInppYFahObbIQOQK3UGxEnyQpltZLaPe75046NOZQikw== "@types/yargs-parser@*": - version "13.0.0" - resolved "https://registry.yarnpkg.com/@types/yargs-parser/-/yargs-parser-13.0.0.tgz#453743c5bbf9f1bed61d959baab5b06be029b2d0" - integrity sha512-wBlsw+8n21e6eTd4yVv8YD/E3xq0O6nNnJIquutAsFGE7EyMKz7W6RNT6BRu1SmdgmlCZ9tb0X+j+D6HGr8pZw== + version "13.1.0" + resolved "https://registry.yarnpkg.com/@types/yargs-parser/-/yargs-parser-13.1.0.tgz#c563aa192f39350a1d18da36c5a8da382bbd8228" + integrity sha512-gCubfBUZ6KxzoibJ+SCUc/57Ms1jz5NjHe4+dI2krNmU5zCPAphyLJYyTOg06ueIyfj+SaCUqmzun7ImlxDcKg== "@types/yargs@^13.0.0": - version "13.0.2" - resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-13.0.2.tgz#a64674fc0149574ecd90ba746e932b5a5f7b3653" - integrity sha512-lwwgizwk/bIIU+3ELORkyuOgDjCh7zuWDFqRtPPhhVgq9N1F7CvLNKg1TX4f2duwtKQ0p044Au9r1PLIXHrIzQ== + version "13.0.3" + resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-13.0.3.tgz#76482af3981d4412d65371a318f992d33464a380" + integrity sha512-K8/LfZq2duW33XW/tFwEAfnZlqIfVsoyRB3kfXdPXYhl0nfM8mmh7GS0jg7WrX2Dgq/0Ha/pR1PaR+BvmWwjiQ== dependencies: "@types/yargs-parser" "*" -"@typescript-eslint/eslint-plugin@1.13.0": - version "1.13.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-1.13.0.tgz#22fed9b16ddfeb402fd7bcde56307820f6ebc49f" - integrity sha512-WQHCozMnuNADiqMtsNzp96FNox5sOVpU8Xt4meaT4em8lOG1SrOv92/mUbEHQVh90sldKSfcOc/I0FOb/14G1g== +"@typescript-eslint/eslint-plugin@^2.2.0": + version "2.4.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-2.4.0.tgz#aaf6b542ff75b78f4191a8bf1c519184817caa24" + integrity sha512-se/YCk7PUoyMwSm/u3Ii9E+BgDUc736uw/lXCDpXEqRgPGsoBTtS8Mntue/vZX8EGyzGplYuePBuVyhZDM9EpQ== dependencies: - "@typescript-eslint/experimental-utils" "1.13.0" - eslint-utils "^1.3.1" + "@typescript-eslint/experimental-utils" "2.4.0" + eslint-utils "^1.4.2" functional-red-black-tree "^1.0.1" regexpp "^2.0.1" - tsutils "^3.7.0" + tsutils "^3.17.1" -"@typescript-eslint/experimental-utils@1.13.0": - version "1.13.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-1.13.0.tgz#b08c60d780c0067de2fb44b04b432f540138301e" - integrity sha512-zmpS6SyqG4ZF64ffaJ6uah6tWWWgZ8m+c54XXgwFtUv0jNz8aJAVx8chMCvnk7yl6xwn8d+d96+tWp7fXzTuDg== +"@typescript-eslint/experimental-utils@2.4.0": + version "2.4.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-2.4.0.tgz#dd8f3f466be25c3610a06fed22cfb6e6aa17f6d9" + integrity sha512-2cvhNaJoWavgTtnC7e1jUSPZQ7e4U2X9Yoy5sQmkS7lTESuyuZrlRcaoNuFfYEd6hgrmMU7+QoSp8Ad+kT1nfA== dependencies: "@types/json-schema" "^7.0.3" - "@typescript-eslint/typescript-estree" "1.13.0" - eslint-scope "^4.0.0" + "@typescript-eslint/typescript-estree" "2.4.0" + eslint-scope "^5.0.0" -"@typescript-eslint/parser@1.13.0": - version "1.13.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-1.13.0.tgz#61ac7811ea52791c47dc9fd4dd4a184fae9ac355" - integrity sha512-ITMBs52PCPgLb2nGPoeT4iU3HdQZHcPaZVw+7CsFagRJHUhyeTgorEwHXhFf3e7Evzi8oujKNpHc8TONth8AdQ== +"@typescript-eslint/parser@^2.2.0": + version "2.4.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-2.4.0.tgz#fe43ed5fec14af03d3594fce2c3b7ec4c8df0243" + integrity sha512-IouAKi/grJ4MFrwdXIJ1GHAwbPWYgkT3b/x8Q49F378c9nwgxVkO76e0rZeUVpwHMaUuoKG2sUeK0XGkwdlwkw== dependencies: "@types/eslint-visitor-keys" "^1.0.0" - "@typescript-eslint/experimental-utils" "1.13.0" - "@typescript-eslint/typescript-estree" "1.13.0" - eslint-visitor-keys "^1.0.0" + "@typescript-eslint/experimental-utils" "2.4.0" + "@typescript-eslint/typescript-estree" "2.4.0" + eslint-visitor-keys "^1.1.0" -"@typescript-eslint/typescript-estree@1.13.0": - version "1.13.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-1.13.0.tgz#8140f17d0f60c03619798f1d628b8434913dc32e" - integrity sha512-b5rCmd2e6DCC6tCTN9GSUAuxdYwCM/k/2wdjHGrIRGPSJotWMCe/dGpi66u42bhuh8q3QBzqM4TMA1GUUCJvdw== +"@typescript-eslint/typescript-estree@2.4.0": + version "2.4.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-2.4.0.tgz#722c95493e1b7682893edaaaec0e69f36917feef" + integrity sha512-/DzDAtMqF5d9IlXrrvu/Id/uoKjnSxf/3FbtKK679a/T7lbDM8qQuirtGvFy6Uh+x0hALuCMwnMfUf0P24/+Iw== dependencies: + chokidar "^3.0.2" + glob "^7.1.4" + is-glob "^4.0.1" lodash.unescape "4.0.1" - semver "5.5.0" + semver "^6.3.0" "@webassemblyjs/ast@1.8.5": version "1.8.5" @@ -1422,9 +1525,9 @@ integrity sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ== abab@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/abab/-/abab-2.0.0.tgz#aba0ab4c5eee2d4c79d3487d85450fb2376ebb0f" - integrity sha512-sY5AXXVZv4Y1VACTtR11UJCPHHudgY5i26Qj5TypE6DKlIApbwb5uqhXcJ5UUGbvZNRh7EeIoW+LrJumBsKp7w== + version "2.0.2" + resolved "https://registry.yarnpkg.com/abab/-/abab-2.0.2.tgz#a2fba1b122c69a85caa02d10f9270c7219709a9d" + integrity sha512-2scffjvioEmNz0OyDSLGWDfKCVwaKc6l9Pm9kOIREU13ClXZvHpg/nRL5xyjSSSLhOnXqft2HpsAzNEEA8cFFg== abbrev@1: version "1.1.1" @@ -1440,17 +1543,17 @@ accepts@~1.3.4, accepts@~1.3.5, accepts@~1.3.7: negotiator "0.6.2" acorn-globals@^4.1.0, acorn-globals@^4.3.0: - version "4.3.3" - resolved "https://registry.yarnpkg.com/acorn-globals/-/acorn-globals-4.3.3.tgz#a86f75b69680b8780d30edd21eee4e0ea170c05e" - integrity sha512-vkR40VwS2SYO98AIeFvzWWh+xyc2qi9s7OoXSFEGIP/rOJKzjnhykaZJNnHdoq4BL2gGxI5EZOU16z896EYnOQ== + version "4.3.4" + resolved "https://registry.yarnpkg.com/acorn-globals/-/acorn-globals-4.3.4.tgz#9fa1926addc11c97308c4e66d7add0d40c3272e7" + integrity sha512-clfQEh21R+D0leSbUdWf3OcfqyaCSAQ8Ryq00bofSekfr9W8u1jyYZo6ir0xu9Gtcf7BjcHJpnbZH7JOCpP60A== dependencies: acorn "^6.0.1" acorn-walk "^6.0.1" -acorn-jsx@^5.0.2: - version "5.0.2" - resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.0.2.tgz#84b68ea44b373c4f8686023a551f61a21b7c4a4f" - integrity sha512-tiNTrP1MP0QrChmD2DdupCr6HWSFeKVw5d/dHTu4Y7rkAkRhU/Dt7dphAfIUyxtHpl/eBVip5uTNSpQJHylpAw== +acorn-jsx@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.1.0.tgz#294adb71b57398b0680015f0a38c563ee1db5384" + integrity sha512-tMUqwBWfLFbJbizRmEcWSLw6HnFzfdJs2sOJEOwwtVPMoH/0Ay+E703oZz78VSXZiiDcZrQ5XKjPIUQixhmgVw== acorn-walk@^6.0.1: version "6.2.0" @@ -1467,17 +1570,12 @@ acorn@^6.0.1, acorn@^6.0.4, acorn@^6.2.1: resolved "https://registry.yarnpkg.com/acorn/-/acorn-6.3.0.tgz#0087509119ffa4fc0a0041d1e93a417e68cb856e" integrity sha512-/czfa8BwS88b9gWQVhc8eknunSA2DoJpJyTQkhheIf5E48u1N0R4q/YxxsAeqRrmK9TQ/uYfgLDfZo91UlANIA== -acorn@^7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.0.0.tgz#26b8d1cd9a9b700350b71c0905546f64d1284e7a" - integrity sha512-PaF/MduxijYYt7unVGRuds1vBC9bFxbNf+VWqhOClfdgy7RlVkQqt610ig1/yxTgsDIfW1cWDel5EBbOy3jdtQ== - -address@1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/address/-/address-1.1.0.tgz#ef8e047847fcd2c5b6f50c16965f924fd99fe709" - integrity sha512-4diPfzWbLEIElVG4AnqP+00SULlPzNuyJFNnmMrLgyaxG6tZXJ1sn7mjBu4fHrJE+Yp/jgylOweJn2xsLMFggQ== +acorn@^7.1.0: + version "7.1.0" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.1.0.tgz#949d36f2c292535da602283586c2477c57eb2d6c" + integrity sha512-kL5CuoXA/dgxlBbVrflsflzQ3PAas7RYZB52NOm/6839iVYJgKMJ3cQJD+t2i5+qFa8h3MDpEOJiS64E8JLnSQ== -address@^1.0.1: +address@1.1.2, address@^1.0.1: version "1.1.2" resolved "https://registry.yarnpkg.com/address/-/address-1.1.2.tgz#bf1116c9c758c51b7a933d296b72c221ed9428b6" integrity sha512-aT6camzM4xEA54YVJYSqxz1kv4IHnQZRtThJJHhUMRExaU5spC7jX5ugSwTaTgJliIgs4VhZOk7htClvQ/LmRA== @@ -1493,7 +1591,7 @@ adjust-sourcemap-loader@2.0.0: object-path "0.11.4" regex-parser "2.2.10" -airbnb-prop-types@^2.13.2: +airbnb-prop-types@^2.15.0: version "2.15.0" resolved "https://registry.yarnpkg.com/airbnb-prop-types/-/airbnb-prop-types-2.15.0.tgz#5287820043af1eb469f5b0af0d6f70da6c52aaef" integrity sha512-jUh2/hfKsRjNFC4XONQrxo/n/3GG4Tn6Hl0WlFQN5PY9OMC9loSCoAYKnZsWaP8wEfd5xcrPloK0Zg6iS1xwVA== @@ -1544,6 +1642,13 @@ ansi-escapes@^3.0.0, ansi-escapes@^3.2.0: resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-3.2.0.tgz#8780b98ff9dbf5638152d1f1fe5c1d7b4442976b" integrity sha512-cBhpre4ma+U0T1oM5fXg7Dy1Jw7zzwv7lt/GoCpr+hDQJoYnKVPLL4dCvSEFMmQurOQvSrwT7SL/DAlhBI97RQ== +ansi-escapes@^4.2.1: + version "4.2.1" + resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-4.2.1.tgz#4dccdb846c3eee10f6d64dea66273eab90c37228" + integrity sha512-Cg3ymMAdN10wOk/VYfLV7KCQyv7EDirJ64500sU7n9UlmioEtDuU5Gd+hj73hXSU/ex7tHJSssmyftDdkMLO8Q== + dependencies: + type-fest "^0.5.2" + ansi-html@0.0.7: version "0.0.7" resolved "https://registry.yarnpkg.com/ansi-html/-/ansi-html-0.0.7.tgz#813584021962a9e9e6fd039f940d12f56ca7859e" @@ -1584,6 +1689,14 @@ anymatch@^2.0.0: micromatch "^3.1.4" normalize-path "^2.1.1" +anymatch@~3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.1.tgz#c55ecf02185e2469259399310c173ce31233b142" + integrity sha512-mM8522psRCqzV+6LhomX5wgp25YVibjh8Wj23I5RPkPppSVSjyKD2A2mBJmWGa+KN7f2D6LNh9jkBCeyLktzjg== + dependencies: + normalize-path "^3.0.0" + picomatch "^2.0.4" + aproba@^1.0.3, aproba@^1.1.1: version "1.2.0" resolved "https://registry.yarnpkg.com/aproba/-/aproba-1.2.0.tgz#6802e6264efd18c790a1b0d517f0f2627bf2c94a" @@ -1642,11 +1755,6 @@ array-filter@^1.0.0: resolved "https://registry.yarnpkg.com/array-filter/-/array-filter-1.0.0.tgz#baf79e62e6ef4c2a4c0b831232daffec251f9d83" integrity sha1-uveeYubvTCpMC4MSMtr/7CUfnYM= -array-filter@~0.0.0: - version "0.0.1" - resolved "https://registry.yarnpkg.com/array-filter/-/array-filter-0.0.1.tgz#7da8cf2e26628ed732803581fd21f67cacd2eeec" - integrity sha1-fajPLiZijtcygDWB/SH2fKzS7uw= - array-flatten@1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2" @@ -1665,16 +1773,6 @@ array-includes@^3.0.3: define-properties "^1.1.2" es-abstract "^1.7.0" -array-map@~0.0.0: - version "0.0.0" - resolved "https://registry.yarnpkg.com/array-map/-/array-map-0.0.0.tgz#88a2bab73d1cf7bcd5c1b118a003f66f665fa662" - integrity sha1-iKK6tz0c97zVwbEYoAP2b2ZfpmI= - -array-reduce@~0.0.0: - version "0.0.0" - resolved "https://registry.yarnpkg.com/array-reduce/-/array-reduce-0.0.0.tgz#173899d3ffd1c7d9383e4479525dbe278cab5f2b" - integrity sha1-FziZ0//Rx9k4PkR5Ul2+J4yrXys= - array-union@^1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/array-union/-/array-union-1.0.2.tgz#9a34410e4f4e3da23dea375be5be70f24778ec39" @@ -1701,12 +1799,12 @@ array.prototype.find@^2.1.0: es-abstract "^1.13.0" array.prototype.flat@^1.2.1: - version "1.2.1" - resolved "https://registry.yarnpkg.com/array.prototype.flat/-/array.prototype.flat-1.2.1.tgz#812db8f02cad24d3fab65dd67eabe3b8903494a4" - integrity sha512-rVqIs330nLJvfC7JqYvEWwqVr5QjYF1ib02i3YJtR/fICO6527Tjpc/e4Mvmxh3GIePPreRXMdaGyC99YphWEw== + version "1.2.2" + resolved "https://registry.yarnpkg.com/array.prototype.flat/-/array.prototype.flat-1.2.2.tgz#8f3c71d245ba349b6b64b4078f76f5576f1fd723" + integrity sha512-VXjh7lAL4KXKF2hY4FnEW9eRW6IhdvFW1sN/JwLbmECbCgACCnBHNyP3lFiYuttr0jxRN9Bsc5+G27dMseSWqQ== dependencies: - define-properties "^1.1.2" - es-abstract "^1.10.0" + define-properties "^1.1.3" + es-abstract "^1.15.0" function-bind "^1.1.1" arrify@^1.0.1: @@ -1780,10 +1878,12 @@ async-limiter@~1.0.0: resolved "https://registry.yarnpkg.com/async-limiter/-/async-limiter-1.0.1.tgz#dd379e94f0db8310b08291f9d64c3209766617fd" integrity sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ== -async@^1.5.2: - version "1.5.2" - resolved "https://registry.yarnpkg.com/async/-/async-1.5.2.tgz#ec6a61ae56480c0c3cb241c95618e20892f9672a" - integrity sha1-7GphrlZIDAw8skHJVhjiCJL5Zyo= +async@^2.6.2: + version "2.6.3" + resolved "https://registry.yarnpkg.com/async/-/async-2.6.3.tgz#d72625e2344a3656e3a3ad4fa749fa83299d82ff" + integrity sha512-zflvls11DCy+dQWzTW2dzuilv8Z5X/pjfmZOWba6TNIVDm+2UDaJmXSOXlasHKfNBs8oo3M0aT50fDEWfKZjXg== + dependencies: + lodash "^4.17.14" asynckit@^0.4.0: version "0.4.0" @@ -1809,17 +1909,17 @@ auth0-js@^9.8.2: winchan "^0.2.2" autoprefixer@^9.6.1: - version "9.6.1" - resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-9.6.1.tgz#51967a02d2d2300bb01866c1611ec8348d355a47" - integrity sha512-aVo5WxR3VyvyJxcJC3h4FKfwCQvQWb1tSI5VHNibddCVWrcD1NvlxEweg3TSgiPztMnWfjpy2FURKA2kvDE+Tw== + version "9.6.5" + resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-9.6.5.tgz#98f4afe7e93cccf323287515d426019619775e5e" + integrity sha512-rGd50YV8LgwFQ2WQp4XzOTG69u1qQsXn0amww7tjqV5jJuNazgFKYEVItEBngyyvVITKOg20zr2V+9VsrXJQ2g== dependencies: - browserslist "^4.6.3" - caniuse-lite "^1.0.30000980" + browserslist "^4.7.0" + caniuse-lite "^1.0.30000999" chalk "^2.4.2" normalize-range "^0.1.2" num2fraction "^1.2.2" - postcss "^7.0.17" - postcss-value-parser "^4.0.0" + postcss "^7.0.18" + postcss-value-parser "^4.0.2" aws-sign2@~0.7.0: version "0.7.0" @@ -1847,17 +1947,17 @@ babel-code-frame@^6.22.0: esutils "^2.0.2" js-tokens "^3.0.2" -babel-eslint@10.0.2: - version "10.0.2" - resolved "https://registry.yarnpkg.com/babel-eslint/-/babel-eslint-10.0.2.tgz#182d5ac204579ff0881684b040560fdcc1558456" - integrity sha512-UdsurWPtgiPgpJ06ryUnuaSXC2s0WoSZnQmEpbAH65XZSdwowgN5MvyP7e88nW07FYXv72erVtpBkxyDVKhH1Q== +babel-eslint@10.0.3: + version "10.0.3" + resolved "https://registry.yarnpkg.com/babel-eslint/-/babel-eslint-10.0.3.tgz#81a2c669be0f205e19462fed2482d33e4687a88a" + integrity sha512-z3U7eMY6r/3f3/JB9mTsLjyxrv0Yb1zb8PCWCLpguxfCzBIZUwy23R1t/XKewP+8mEN2Ck8Dtr4q20z6ce6SoA== dependencies: "@babel/code-frame" "^7.0.0" "@babel/parser" "^7.0.0" "@babel/traverse" "^7.0.0" "@babel/types" "^7.0.0" - eslint-scope "3.7.1" eslint-visitor-keys "^1.0.0" + resolve "^1.12.0" babel-extract-comments@^1.0.0: version "1.0.0" @@ -1866,7 +1966,7 @@ babel-extract-comments@^1.0.0: dependencies: babylon "^6.18.0" -babel-jest@^24.8.0, babel-jest@^24.9.0: +babel-jest@^24.9.0: version "24.9.0" resolved "https://registry.yarnpkg.com/babel-jest/-/babel-jest-24.9.0.tgz#3fc327cb8467b89d14d7bc70e315104a783ccd54" integrity sha512-ntuddfyiN+EhMw58PTNL1ph4C9rECiQXjI4nMMBKBaNjXvqLdkXpPRcMSr4iyBrJg/+wz9brFUD6RhOAT6r4Iw== @@ -1922,10 +2022,10 @@ babel-plugin-macros@2.6.1: cosmiconfig "^5.2.0" resolve "^1.10.0" -babel-plugin-named-asset-import@^0.3.3: - version "0.3.3" - resolved "https://registry.yarnpkg.com/babel-plugin-named-asset-import/-/babel-plugin-named-asset-import-0.3.3.tgz#9ba2f3ac4dc78b042651654f07e847adfe50667c" - integrity sha512-1XDRysF4894BUdMChT+2HHbtJYiO7zx5Be7U6bT8dISy7OdyETMGIAQBMPQCsY1YRf0xcubwnKKaDr5bk15JTA== +babel-plugin-named-asset-import@^0.3.4: + version "0.3.4" + resolved "https://registry.yarnpkg.com/babel-plugin-named-asset-import/-/babel-plugin-named-asset-import-0.3.4.tgz#4a8fc30e9a3e2b1f5ed36883386ab2d84e1089bd" + integrity sha512-S6d+tEzc5Af1tKIMbsf2QirCcPdQ+mKUCY2H1nJj1DyA1ShwpsoxEOAwbWsG5gcXNV/olpvQd9vrUWRx4bnhpw== babel-plugin-syntax-object-rest-spread@^6.8.0: version "6.13.0" @@ -1953,24 +2053,24 @@ babel-preset-jest@^24.9.0: "@babel/plugin-syntax-object-rest-spread" "^7.0.0" babel-plugin-jest-hoist "^24.9.0" -babel-preset-react-app@^9.0.1: - version "9.0.1" - resolved "https://registry.yarnpkg.com/babel-preset-react-app/-/babel-preset-react-app-9.0.1.tgz#16a2cf84363045b530b6a03460527a5c6eac42ba" - integrity sha512-v7MeY+QxdBhM9oU5uOQCIHLsErYkEbbjctXsb10II+KAnttbe0rvprvP785dRxfa9dI4ZbsGXsRU07Qdi5BtOw== +babel-preset-react-app@^9.0.2: + version "9.0.2" + resolved "https://registry.yarnpkg.com/babel-preset-react-app/-/babel-preset-react-app-9.0.2.tgz#247d37e883d6d6f4b4691e5f23711bb2dd80567d" + integrity sha512-aXD+CTH8Chn8sNJr4tO/trWKqe5sSE4hdO76j9fhVezJSzmpWYWUSc5JoPmdSxADwef5kQFNGKXd433vvkd2VQ== dependencies: - "@babel/core" "7.5.5" + "@babel/core" "7.6.0" "@babel/plugin-proposal-class-properties" "7.5.5" - "@babel/plugin-proposal-decorators" "7.4.4" + "@babel/plugin-proposal-decorators" "7.6.0" "@babel/plugin-proposal-object-rest-spread" "7.5.5" "@babel/plugin-syntax-dynamic-import" "7.2.0" - "@babel/plugin-transform-destructuring" "7.5.0" + "@babel/plugin-transform-destructuring" "7.6.0" "@babel/plugin-transform-flow-strip-types" "7.4.4" "@babel/plugin-transform-react-display-name" "7.2.0" - "@babel/plugin-transform-runtime" "7.5.5" - "@babel/preset-env" "7.5.5" + "@babel/plugin-transform-runtime" "7.6.0" + "@babel/preset-env" "7.6.0" "@babel/preset-react" "7.0.0" - "@babel/preset-typescript" "7.3.3" - "@babel/runtime" "7.5.5" + "@babel/preset-typescript" "7.6.0" + "@babel/runtime" "7.6.0" babel-plugin-dynamic-import-node "2.3.0" babel-plugin-macros "2.6.1" babel-plugin-transform-react-remove-prop-types "0.4.24" @@ -2038,10 +2138,15 @@ binary-extensions@^1.0.0: resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-1.13.1.tgz#598afe54755b2868a5330d2aff9d4ebb53209b65" integrity sha512-Un7MIEDdUC5gNpcGDV97op1Ywk748MpHcFTHoYs6qnj1Z3j7I53VG3nwZhKzoBZmbdRNnb6WRdFlwl7tSDuZGw== +binary-extensions@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.0.0.tgz#23c0df14f6a88077f5f986c0d167ec03c3d5537c" + integrity sha512-Phlt0plgpIIBOGTT/ehfFnbNlfsDEiqmzE2KRXoX1bLIlir4X/MR+zSyBEkL05ffWgnRSf/DXv+WrUAVr93/ow== + bluebird@^3.5.5: - version "3.5.5" - resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.5.5.tgz#a8d0afd73251effbbd5fe384a77d73003c17a71f" - integrity sha512-5am6HnnfN+urzt4yfg7IgTbotDjIT/u8AJpEt0sIU9FtXfVeezXAPKswrG+xKUCOYAINpSdgZVDU6QFh+cuH3w== + version "3.7.1" + resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.1.tgz#df70e302b471d7473489acf26a93d63b53f874de" + integrity sha512-DdmyoGCleJnkbp3nkbxTLJ18rjDsE4yCggEwKNXkeV123sPNfOCYeDoeuOY+F2FrSjO1YXcTU+dsy96KMy+gcg== bn.js@^4.0.0, bn.js@^4.1.0, bn.js@^4.1.1, bn.js@^4.4.0: version "4.11.8" @@ -2110,6 +2215,13 @@ braces@^2.3.1, braces@^2.3.2: split-string "^3.0.2" to-regex "^3.0.1" +braces@~3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107" + integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A== + dependencies: + fill-range "^7.0.1" + brorand@^1.0.1: version "1.1.0" resolved "https://registry.yarnpkg.com/brorand/-/brorand-1.1.0.tgz#12c25efe40a45e3c323eb8675a0a0ce57b22371f" @@ -2186,14 +2298,23 @@ browserify-zlib@^0.2.0: dependencies: pako "~1.0.5" -browserslist@4.6.6, browserslist@^4.0.0, browserslist@^4.1.1, browserslist@^4.6.0, browserslist@^4.6.3, browserslist@^4.6.4, browserslist@^4.6.6: - version "4.6.6" - resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.6.6.tgz#6e4bf467cde520bc9dbdf3747dafa03531cec453" - integrity sha512-D2Nk3W9JL9Fp/gIcWei8LrERCS+eXu9AM5cfXA8WEZ84lFks+ARnZ0q/R69m2SV3Wjma83QDDPxsNKXUwdIsyA== +browserslist@4.7.0: + version "4.7.0" + resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.7.0.tgz#9ee89225ffc07db03409f2fee524dc8227458a17" + integrity sha512-9rGNDtnj+HaahxiVV38Gn8n8Lr8REKsel68v1sPFfIGEK6uSXTY3h9acgiT1dZVtOOUtifo/Dn8daDQ5dUgVsA== + dependencies: + caniuse-lite "^1.0.30000989" + electron-to-chromium "^1.3.247" + node-releases "^1.1.29" + +browserslist@^4.0.0, browserslist@^4.1.1, browserslist@^4.6.0, browserslist@^4.6.4, browserslist@^4.7.0: + version "4.7.1" + resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.7.1.tgz#bd400d1aea56538580e8c4d5f1c54ac11b5ab468" + integrity sha512-QtULFqKIAtiyNx7NhZ/p4rB8m3xDozVo/pi5VgTlADLF2tNigz/QH+v0m5qhn7XfHT7u+607NcCNOnC0HZAlMg== dependencies: - caniuse-lite "^1.0.30000984" - electron-to-chromium "^1.3.191" - node-releases "^1.1.25" + caniuse-lite "^1.0.30000999" + electron-to-chromium "^1.3.284" + node-releases "^1.1.36" bser@^2.0.0: version "2.1.0" @@ -2339,10 +2460,10 @@ caniuse-api@^3.0.0: lodash.memoize "^4.1.2" lodash.uniq "^4.5.0" -caniuse-lite@^1.0.0, caniuse-lite@^1.0.30000980, caniuse-lite@^1.0.30000981, caniuse-lite@^1.0.30000984: - version "1.0.30000989" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30000989.tgz#b9193e293ccf7e4426c5245134b8f2a56c0ac4b9" - integrity sha512-vrMcvSuMz16YY6GSVZ0dWDTJP8jqk3iFQ/Aq5iqblPwxSVVZI+zxDyTX0VPqtQsDnfdrBDcsmhgTEOh5R8Lbpw== +caniuse-lite@^1.0.0, caniuse-lite@^1.0.30000981, caniuse-lite@^1.0.30000989, caniuse-lite@^1.0.30000999: + version "1.0.30001002" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001002.tgz#ba999a737b1abd5bf0fd47efe43a09b9cadbe9b0" + integrity sha512-pRuxPE8wdrWmVPKcDmJJiGBxr6lFJq4ivdSeo9FTmGj5Rb8NX3Mby2pARG57MXF15hYAhZ0nHV5XxT2ig4bz3g== capture-exit@^2.0.0: version "2.0.0" @@ -2417,10 +2538,25 @@ chokidar@^2.0.0, chokidar@^2.0.2, chokidar@^2.0.4: optionalDependencies: fsevents "^1.2.7" +chokidar@^3.0.2: + version "3.2.2" + resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.2.2.tgz#a433973350021e09f2b853a2287781022c0dc935" + integrity sha512-bw3pm7kZ2Wa6+jQWYP/c7bAZy3i4GwiIiMO2EeRjrE48l8vBqC/WvFhSF0xyM8fQiPEGvwMY/5bqDG7sSEOuhg== + dependencies: + anymatch "~3.1.1" + braces "~3.0.2" + glob-parent "~5.1.0" + is-binary-path "~2.1.0" + is-glob "~4.0.1" + normalize-path "~3.0.0" + readdirp "~3.2.0" + optionalDependencies: + fsevents "~2.1.1" + chownr@^1.1.1: - version "1.1.2" - resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.1.2.tgz#a18f1e0b269c8a6a5d3c86eb298beb14c3dd7bf6" - integrity sha512-GkfeAQh+QNy3wquu9oIZr6SS5x7wGdSgNQvD10X3r+AZr1Oys22HW8kAmDMvNg2+Dm0TeGaEuO8gFwdBXxwO8A== + version "1.1.3" + resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.1.3.tgz#42d837d5239688d55f303003a508230fa6727142" + integrity sha512-i70fVHhmV3DtTl6nqvZOnIjbY0Pe4kAUjwHj8z0zAdgBtYrJyYwLKCCuRBQ5ppkyL0AkN7HKRnETdmdp1zqNXw== chrome-trace-event@^1.0.2: version "1.0.2" @@ -2591,10 +2727,10 @@ commander@2.17.x: resolved "https://registry.yarnpkg.com/commander/-/commander-2.17.1.tgz#bd77ab7de6de94205ceacc72f1716d29f20a77bf" integrity sha512-wPMUt6FnH2yzG95SA6mzjQOEKUU3aLaDEmzs1ti+1E9h+CsrZghRlqEM/EJ4KscsQVG8uNN4uVreUeT8+drlgg== -commander@^2.11.0, commander@^2.19.0, commander@^2.20.0, commander@~2.20.0: - version "2.20.0" - resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.0.tgz#d58bb2b5c1ee8f87b0d340027e9e94e222c5a422" - integrity sha512-7j2y+40w61zy6YC2iRNpUe/NwhNyoXrYpHMrSunaMG64nRnaf96zO/KMQR4OyN/UnE5KLyEBnKHd4aG3rskjpQ== +commander@^2.11.0, commander@^2.19.0, commander@^2.20.0, commander@~2.20.3: + version "2.20.3" + resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33" + integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ== commander@~2.19.0: version "2.19.0" @@ -2658,10 +2794,10 @@ concat-stream@^1.5.0: readable-stream "^2.2.2" typedarray "^0.0.6" -confusing-browser-globals@^1.0.8: - version "1.0.8" - resolved "https://registry.yarnpkg.com/confusing-browser-globals/-/confusing-browser-globals-1.0.8.tgz#93ffec1f82a6e2bf2bc36769cc3a92fa20e502f3" - integrity sha512-lI7asCibVJ6Qd3FGU7mu4sfG4try4LX3+GVS+Gv8UlrEf2AeW57piecapnog2UHZSbcX/P/1UDWVaTsblowlZg== +confusing-browser-globals@^1.0.9: + version "1.0.9" + resolved "https://registry.yarnpkg.com/confusing-browser-globals/-/confusing-browser-globals-1.0.9.tgz#72bc13b483c0276801681871d4898516f8f54fdd" + integrity sha512-KbS1Y0jMtyPgIxjO7ZzMAuUpAKMt1SzCL9fsrKsX6b0zJPTaT0SiSPmewwVZg9UAO83HVIlEhZF84LIjZ0lmAw== connect-history-api-fallback@^1.3.0: version "1.6.0" @@ -2747,17 +2883,17 @@ copy-descriptor@^0.1.0: integrity sha1-Z29us8OZl8LuGsOpJP1hJHSPV40= core-js-compat@^3.1.1: - version "3.2.1" - resolved "https://registry.yarnpkg.com/core-js-compat/-/core-js-compat-3.2.1.tgz#0cbdbc2e386e8e00d3b85dc81c848effec5b8150" - integrity sha512-MwPZle5CF9dEaMYdDeWm73ao/IflDH+FjeJCWEADcEgFSE9TLimFKwJsfmkwzI8eC0Aj0mgvMDjeQjrElkz4/A== + version "3.3.2" + resolved "https://registry.yarnpkg.com/core-js-compat/-/core-js-compat-3.3.2.tgz#1096c989c1b929ede06b5b6b4768dc4439078c03" + integrity sha512-gfiK4QnNXhnnHVOIZst2XHdFfdMTPxtR0EGs0TdILMlGIft+087oH6/Sw2xTTIjpWXC9vEwsJA8VG3XTGcmO5g== dependencies: - browserslist "^4.6.6" + browserslist "^4.7.0" semver "^6.3.0" -core-js@3.1.4: - version "3.1.4" - resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.1.4.tgz#3a2837fc48e582e1ae25907afcd6cf03b0cc7a07" - integrity sha512-YNZN8lt82XIMLnLirj9MhKDFZHalwzzrL9YLt6eb0T5D0EDl4IQ90IGkua8mHbnxNrkj1d8hbdizMc0Qmg1WnQ== +core-js@3.2.1: + version "3.2.1" + resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.2.1.tgz#cd41f38534da6cc59f7db050fe67307de9868b09" + integrity sha512-Qa5XSVefSVPRxy2XfUC13WbvqkxhkwB3ve+pgCQveNgYzbM/UxZeu1dcOX/xr4UmfUd+muuvsaxilQzCyUurMw== core-js@^1.0.0: version "1.2.7" @@ -2765,9 +2901,9 @@ core-js@^1.0.0: integrity sha1-ZSKUwUZR2yj6k70tX/KYOk8IxjY= core-js@^2.4.0: - version "2.6.9" - resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.6.9.tgz#6b4b214620c834152e179323727fc19741b084f2" - integrity sha512-HOpZf6eXmnl7la+cUdMnLvUxKNqLUzJvgIziQ0DiF3JwSImNphIqdGqzj6hIKyX04MmV0poclQ7+wjWvxQyR2A== + version "2.6.10" + resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.6.10.tgz#8a5b8391f8cc7013da703411ce5b585706300d7f" + integrity sha512-I39t74+4t+zau64EN1fE5v2W31Adtc/REhzWN+gWRRXg6WH5qAsZm62DHpQ1+Yhe4047T55jvzz7MUqF/dBBlA== core-util-is@1.0.2, core-util-is@~1.0.0: version "1.0.2" @@ -3071,12 +3207,12 @@ cssstyle@^1.0.0, cssstyle@^1.1.1: dependencies: cssom "0.3.x" -cyclist@~0.2.2: - version "0.2.2" - resolved "https://registry.yarnpkg.com/cyclist/-/cyclist-0.2.2.tgz#1b33792e11e914a2fd6d6ed6447464444e5fa640" - integrity sha1-GzN5LhHpFKL9bW7WRHRkRE5fpkA= +cyclist@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/cyclist/-/cyclist-1.0.1.tgz#596e9698fd0c80e12038c2b82d6eb1b35b6224d9" + integrity sha1-WW6WmP0MgOEgOMK4LW6xs1tiJNk= -d@1: +d@1, d@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/d/-/d-1.0.1.tgz#8698095372d58dbee346ffd0c7093f99f8f9eb5a" integrity sha512-m62ShEObQ39CfralilEQRjH6oAMtNCV1xJyEx5LpRYUVN+EviphDgUc/F3hnYbADmkiNs67Y+3ylmlG7Lnu+FA== @@ -3117,7 +3253,7 @@ debug@2.6.9, debug@^2.2.0, debug@^2.3.3, debug@^2.6.0, debug@^2.6.8, debug@^2.6. dependencies: ms "2.0.0" -debug@^3.0.0, debug@^3.1.0, debug@^3.2.5, debug@^3.2.6: +debug@^3.0.0, debug@^3.1.0, debug@^3.1.1, debug@^3.2.5, debug@^3.2.6: version "3.2.6" resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.6.tgz#e83d17de16d8a7efb7717edbe5fb10135eee629b" integrity sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ== @@ -3433,10 +3569,10 @@ dot-prop@^4.1.1: dependencies: is-obj "^1.0.0" -dotenv-expand@4.2.0: - version "4.2.0" - resolved "https://registry.yarnpkg.com/dotenv-expand/-/dotenv-expand-4.2.0.tgz#def1f1ca5d6059d24a766e587942c21106ce1275" - integrity sha1-3vHxyl1gWdJKdm5YeULCEQbOEnU= +dotenv-expand@5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/dotenv-expand/-/dotenv-expand-5.1.0.tgz#3fbaf020bfd794884072ea26b1e9791d45a629f0" + integrity sha512-YXQl1DSa4/PQyRfgrv6aoNjhasp/p4qs9FjJ4q4cQk+8m4r6k4ZSiEyytKG8f8W9gi8WsQtIObNmKd+tMzNTmA== dotenv@6.2.0: version "6.2.0" @@ -3471,15 +3607,15 @@ ee-first@1.1.1: resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" integrity sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0= -electron-to-chromium@^1.3.191: - version "1.3.243" - resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.243.tgz#32f64f00fa121532d1d49f5c0a15fd77f52ae889" - integrity sha512-+edFdHGxLSmAKftXa5xZIg19rHkkJLiW+tRu0VMVG3RKztyeKX7d3pXf707lS6+BxB9uBun3RShbxCI1PtBAgQ== +electron-to-chromium@^1.3.247, electron-to-chromium@^1.3.284: + version "1.3.289" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.289.tgz#1f85add5d7086ce95d9361348c26aa9de5779906" + integrity sha512-39GEOWgTxtMDk/WjIQLg4W/l1s4FZdiMCqUBLjd92tAXsBPDFLwuwCba5OGhuTdVYm6E128TZIqSnMpeocUlCQ== elliptic@^6.0.0: - version "6.5.0" - resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.5.0.tgz#2b8ed4c891b7de3200e14412a5b8248c7af505ca" - integrity sha512-eFOJTMyCYb7xtE/caJ6JJu+bhi67WCYNbkGSknu20pmM8Ke/bqOfdnZWxyoGN26JgfxTbXrsCkEw4KheCT/KGg== + version "6.5.1" + resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.5.1.tgz#c380f5f909bf1b9b4428d028cd18d3b0efd6b52b" + integrity sha512-xvJINNLbTeWQjrl6X+7eQCrIy/YPv5XCpKW6kB5mKvtnGILoLDcySuwomfdzt0BMdLNVnuRNTuzKNHj0bva1Cg== dependencies: bn.js "^4.4.0" brorand "^1.0.1" @@ -3512,19 +3648,19 @@ encoding@^0.1.11: iconv-lite "~0.4.13" end-of-stream@^1.0.0, end-of-stream@^1.1.0: - version "1.4.1" - resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.1.tgz#ed29634d19baba463b6ce6b80a37213eab71ec43" - integrity sha512-1MkrZNvWTKCaigbn+W15elq2BB/L22nqrSY5DKlo3X6+vclJm8Bb5djXJBmEX6fS3+zCh/F4VBK5Z2KxJt4s2Q== + version "1.4.4" + resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.4.tgz#5ae64a5f45057baf3626ec14da0ca5e4b2431eb0" + integrity sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q== dependencies: once "^1.4.0" enhanced-resolve@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-4.1.0.tgz#41c7e0bfdfe74ac1ffe1e57ad6a5c6c9f3742a7f" - integrity sha512-F/7vkyTtyc/llOIn8oWclcB25KdRaiPBpZYDgJHgh/UHtpgT2p2eldQgtQnLtUvfMKPKxbRaQM/hHkvLHt1Vng== + version "4.1.1" + resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-4.1.1.tgz#2937e2b8066cd0fe7ce0990a98f0d71a35189f66" + integrity sha512-98p2zE+rL7/g/DzMHMTF4zZlCgeVdJ7yr6xzEpJRYwFYrGi9ANdn5DnJURg6RpBkyk60XYDnWIv51VfIhfNGuA== dependencies: graceful-fs "^4.1.2" - memory-fs "^0.4.0" + memory-fs "^0.5.0" tapable "^1.0.0" entities@^1.1.1, entities@~1.1.1: @@ -3537,46 +3673,55 @@ entities@^2.0.0: resolved "https://registry.yarnpkg.com/entities/-/entities-2.0.0.tgz#68d6084cab1b079767540d80e56a39b423e4abf4" integrity sha512-D9f7V0JSRwIxlRI2mjMqufDrRDnx8p+eEOz7aUM9SuvF8gsBzra0/6tbjl1m8eQHrZlYj6PxqE00hZ1SAIKPLw== -enzyme-adapter-react-16@^1.14.0: - version "1.14.0" - resolved "https://registry.yarnpkg.com/enzyme-adapter-react-16/-/enzyme-adapter-react-16-1.14.0.tgz#204722b769172bcf096cb250d33e6795c1f1858f" - integrity sha512-7PcOF7pb4hJUvjY7oAuPGpq3BmlCig3kxXGi2kFx0YzJHppqX1K8IIV9skT1IirxXlu8W7bneKi+oQ10QRnhcA== +enzyme-adapter-react-16@^1.15.1: + version "1.15.1" + resolved "https://registry.yarnpkg.com/enzyme-adapter-react-16/-/enzyme-adapter-react-16-1.15.1.tgz#8ad55332be7091dc53a25d7d38b3485fc2ba50d5" + integrity sha512-yMPxrP3vjJP+4wL/qqfkT6JAIctcwKF+zXO6utlGPgUJT2l4tzrdjMDWGd/Pp1BjHBcljhN24OzNEGRteibJhA== dependencies: - enzyme-adapter-utils "^1.12.0" + enzyme-adapter-utils "^1.12.1" + enzyme-shallow-equal "^1.0.0" has "^1.0.3" object.assign "^4.1.0" object.values "^1.1.0" prop-types "^15.7.2" - react-is "^16.8.6" + react-is "^16.10.2" react-test-renderer "^16.0.0-0" semver "^5.7.0" -enzyme-adapter-utils@^1.12.0: - version "1.12.0" - resolved "https://registry.yarnpkg.com/enzyme-adapter-utils/-/enzyme-adapter-utils-1.12.0.tgz#96e3730d76b872f593e54ce1c51fa3a451422d93" - integrity sha512-wkZvE0VxcFx/8ZsBw0iAbk3gR1d9hK447ebnSYBf95+r32ezBq+XDSAvRErkc4LZosgH8J7et7H7/7CtUuQfBA== +enzyme-adapter-utils@^1.12.1: + version "1.12.1" + resolved "https://registry.yarnpkg.com/enzyme-adapter-utils/-/enzyme-adapter-utils-1.12.1.tgz#e828e0d038e2b1efa4b9619ce896226f85c9dd88" + integrity sha512-KWiHzSjZaLEoDCOxY8Z1RAbUResbqKN5bZvenPbfKtWorJFVETUw754ebkuCQ3JKm0adx1kF8JaiR+PHPiP47g== dependencies: - airbnb-prop-types "^2.13.2" - function.prototype.name "^1.1.0" + airbnb-prop-types "^2.15.0" + function.prototype.name "^1.1.1" object.assign "^4.1.0" - object.fromentries "^2.0.0" + object.fromentries "^2.0.1" prop-types "^15.7.2" - semver "^5.6.0" + semver "^5.7.0" -enzyme-matchers@^7.1.0: - version "7.1.0" - resolved "https://registry.yarnpkg.com/enzyme-matchers/-/enzyme-matchers-7.1.0.tgz#7224298028679af41d224fdff02b9a9a61f2ad86" - integrity sha512-PCfIvyNnZh4ougBaMXKeqNrN6yilzkbtphpi2X74uFeqbPabDUcvNq30Bcw29sSIv7QI1ZCTlQ2ktGep/4jIBw== +enzyme-matchers@^7.1.1: + version "7.1.1" + resolved "https://registry.yarnpkg.com/enzyme-matchers/-/enzyme-matchers-7.1.1.tgz#d1210ce0bd55b55d61af1ff72777bd8b1fa5176e" + integrity sha512-fw/FxwpEg6n1KYpEHnhA44iFduYHDUVVePXSMmf883q/JDMXb+sIU55maSw2oFWqt9zd7rcGqmSV8sHQO5pReg== dependencies: circular-json-es6 "^2.0.1" deep-equal-ident "^1.1.1" +enzyme-shallow-equal@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/enzyme-shallow-equal/-/enzyme-shallow-equal-1.0.0.tgz#d8e4603495e6ea279038eef05a4bf4887b55dc69" + integrity sha512-VUf+q5o1EIv2ZaloNQQtWCJM9gpeux6vudGVH6vLmfPXFLRuxl5+Aq3U260wof9nn0b0i+P5OEUXm1vnxkRpXQ== + dependencies: + has "^1.0.3" + object-is "^1.0.1" + enzyme-to-json@^3.3.0: - version "3.4.0" - resolved "https://registry.yarnpkg.com/enzyme-to-json/-/enzyme-to-json-3.4.0.tgz#2b6330a784a57ba68298e3c0d6cef17ee4fedc0e" - integrity sha512-gbu8P8PMAtb+qtKuGVRdZIYxWHC03q1dGS3EKRmUzmTDIracu3o6cQ0d4xI2YWojbelbxjYOsmqM5EgAL0WgIA== + version "3.4.3" + resolved "https://registry.yarnpkg.com/enzyme-to-json/-/enzyme-to-json-3.4.3.tgz#ed4386f48768ed29e2d1a2910893542c34e7e0af" + integrity sha512-jqNEZlHqLdz7OTpXSzzghArSS3vigj67IU/fWkPyl1c0TCj9P5s6Ze0kRkYZWNEoCqCR79xlQbigYlMx5erh8A== dependencies: - lodash "^4.17.12" + lodash "^4.17.15" enzyme@^3.10.0: version "3.10.0" @@ -3619,17 +3764,21 @@ error-ex@^1.2.0, error-ex@^1.3.1: dependencies: is-arrayish "^0.2.1" -es-abstract@^1.10.0, es-abstract@^1.11.0, es-abstract@^1.12.0, es-abstract@^1.13.0, es-abstract@^1.5.1, es-abstract@^1.7.0: - version "1.13.0" - resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.13.0.tgz#ac86145fdd5099d8dd49558ccba2eaf9b88e24e9" - integrity sha512-vDZfg/ykNxQVwup/8E1BZhVzFfBxs9NqMzGcvIJrqg5k2/5Za2bWo40dK2J1pgLngZ7c+Shh8lwYtLGyrwPutg== +es-abstract@^1.12.0, es-abstract@^1.13.0, es-abstract@^1.15.0, es-abstract@^1.5.1, es-abstract@^1.7.0: + version "1.16.0" + resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.16.0.tgz#d3a26dc9c3283ac9750dca569586e976d9dcc06d" + integrity sha512-xdQnfykZ9JMEiasTAJZJdMWCQ1Vm00NBw79/AWi7ELfZuuPCSOMDZbT9mkOfSctVtfhb+sAAzrm+j//GjjLHLg== dependencies: es-to-primitive "^1.2.0" function-bind "^1.1.1" has "^1.0.3" + has-symbols "^1.0.0" is-callable "^1.1.4" is-regex "^1.0.4" - object-keys "^1.0.12" + object-inspect "^1.6.0" + object-keys "^1.1.1" + string.prototype.trimleft "^2.1.0" + string.prototype.trimright "^2.1.0" es-to-primitive@^1.2.0: version "1.2.0" @@ -3640,10 +3789,10 @@ es-to-primitive@^1.2.0: is-date-object "^1.0.1" is-symbol "^1.0.2" -es5-ext@^0.10.35, es5-ext@^0.10.50, es5-ext@~0.10.14: - version "0.10.50" - resolved "https://registry.yarnpkg.com/es5-ext/-/es5-ext-0.10.50.tgz#6d0e23a0abdb27018e5ac4fd09b412bc5517a778" - integrity sha512-KMzZTPBkeQV/JcSQhI5/z6d9VWJ3EnQ194USTUwIYZ2ZbpN8+SGXQKt1h68EX44+qt+Fzr8DO17vnxrw7c3agw== +es5-ext@^0.10.35, es5-ext@^0.10.50, es5-ext@^0.10.51: + version "0.10.51" + resolved "https://registry.yarnpkg.com/es5-ext/-/es5-ext-0.10.51.tgz#ed2d7d9d48a12df86e0299287e93a09ff478842f" + integrity sha512-oRpWzM2WcLHVKpnrcyB7OW8j/s67Ba04JCm0WnNv3RiABSvs7mrQlutB8DBv793gKcp0XENR8Il8WxGTlZ73gQ== dependencies: es6-iterator "~2.0.3" es6-symbol "~3.1.1" @@ -3659,12 +3808,12 @@ es6-iterator@2.0.3, es6-iterator@~2.0.3: es6-symbol "^3.1.1" es6-symbol@^3.1.1, es6-symbol@~3.1.1: - version "3.1.1" - resolved "https://registry.yarnpkg.com/es6-symbol/-/es6-symbol-3.1.1.tgz#bf00ef4fdab6ba1b46ecb7b629b4c7ed5715cc77" - integrity sha1-vwDvT9q2uhtG7Le2KbTH7VcVzHc= + version "3.1.2" + resolved "https://registry.yarnpkg.com/es6-symbol/-/es6-symbol-3.1.2.tgz#859fdd34f32e905ff06d752e7171ddd4444a7ed1" + integrity sha512-/ZypxQsArlv+KHpGvng52/Iz8by3EQPxhmbuz8yFG89N/caTFBSbcXONDw0aMjy827gQg26XAjP4uXFvnfINmQ== dependencies: - d "1" - es5-ext "~0.10.14" + d "^1.0.1" + es5-ext "^0.10.51" escape-html@~1.0.3: version "1.0.3" @@ -3688,12 +3837,12 @@ escodegen@^1.11.0, escodegen@^1.9.1: optionalDependencies: source-map "~0.6.1" -eslint-config-react-app@^5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/eslint-config-react-app/-/eslint-config-react-app-5.0.1.tgz#5f3d666ba3ee3cb384eb943e260e868f6c72251b" - integrity sha512-GYXP3F/0PSHlYfGHhahqnJze8rYKxzXgrzXVqRRd4rDO40ga4NA3aHM7/HKbwceDN0/C1Ij3BoAWFawJgRbXEw== +eslint-config-react-app@^5.0.2: + version "5.0.2" + resolved "https://registry.yarnpkg.com/eslint-config-react-app/-/eslint-config-react-app-5.0.2.tgz#df40d73a1402986030680c040bbee520db5a32a4" + integrity sha512-VhlESAQM83uULJ9jsvcKxx2Ab0yrmjUt8kDz5DyhTQufqWE0ssAnejlWri5LXv25xoXfdqOyeDPdfJS9dXKagQ== dependencies: - confusing-browser-globals "^1.0.8" + confusing-browser-globals "^1.0.9" eslint-import-resolver-node@^0.3.2: version "0.3.2" @@ -3703,16 +3852,16 @@ eslint-import-resolver-node@^0.3.2: debug "^2.6.9" resolve "^1.5.0" -eslint-loader@2.2.1: - version "2.2.1" - resolved "https://registry.yarnpkg.com/eslint-loader/-/eslint-loader-2.2.1.tgz#28b9c12da54057af0845e2a6112701a2f6bf8337" - integrity sha512-RLgV9hoCVsMLvOxCuNjdqOrUqIj9oJg8hF44vzJaYqsAHuY9G2YAeN3joQ9nxP0p5Th9iFSIpKo+SD8KISxXRg== +eslint-loader@3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/eslint-loader/-/eslint-loader-3.0.2.tgz#5a627316a51d6f41d357b9f6f0554e91506cdd6e" + integrity sha512-S5VnD+UpVY1PyYRqeBd/4pgsmkvSokbHqTXAQMpvCyRr3XN2tvSLo9spm2nEpqQqh9dezw3os/0zWihLeOg2Rw== dependencies: - loader-fs-cache "^1.0.0" - loader-utils "^1.0.2" - object-assign "^4.0.1" - object-hash "^1.1.4" - rimraf "^2.6.1" + fs-extra "^8.1.0" + loader-fs-cache "^1.0.2" + loader-utils "^1.2.3" + object-hash "^1.3.1" + schema-utils "^2.2.0" eslint-module-utils@^2.4.0: version "2.4.1" @@ -3781,15 +3930,7 @@ eslint-plugin-react@7.14.3: prop-types "^15.7.2" resolve "^1.10.1" -eslint-scope@3.7.1: - version "3.7.1" - resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-3.7.1.tgz#3d63c3edfda02e06e01a452ad88caacc7cdcb6e8" - integrity sha1-PWPD7f2gLgbgGkUq2IyqzHzctug= - dependencies: - esrecurse "^4.1.0" - estraverse "^4.1.1" - -eslint-scope@^4.0.0, eslint-scope@^4.0.3: +eslint-scope@^4.0.3: version "4.0.3" resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-4.0.3.tgz#ca03833310f6889a3264781aa82e63eb9cfe7848" integrity sha512-p7VutNr1O/QrxysMo3E45FjYDTeXBy0iTltPFNSqKAIfjDSXC+4dj+qfyuD8bfAXrW/y6lW3O76VaYNPKfpKrg== @@ -3805,12 +3946,12 @@ eslint-scope@^5.0.0: esrecurse "^4.1.0" estraverse "^4.1.1" -eslint-utils@^1.3.1, eslint-utils@^1.4.2: - version "1.4.2" - resolved "https://registry.yarnpkg.com/eslint-utils/-/eslint-utils-1.4.2.tgz#166a5180ef6ab7eb462f162fd0e6f2463d7309ab" - integrity sha512-eAZS2sEUMlIeCjBeubdj45dmBHQwPHWyBcT1VSYB7o9x9WRRqKxyUoiXlRjyAwzN7YEzHJlYg0NmzDRWx6GP4Q== +eslint-utils@^1.4.2: + version "1.4.3" + resolved "https://registry.yarnpkg.com/eslint-utils/-/eslint-utils-1.4.3.tgz#74fec7c54d0776b6f67e0251040b5806564e981f" + integrity sha512-fbBN5W2xdY45KulGXmLHZ3c3FHfVYmKg0IrAKGOkT/464PQsx2UeIzfz1RmEci+KLm1bBaAzZAh8+/E+XAeZ8Q== dependencies: - eslint-visitor-keys "^1.0.0" + eslint-visitor-keys "^1.1.0" eslint-visitor-keys@^1.0.0, eslint-visitor-keys@^1.1.0: version "1.1.0" @@ -3818,9 +3959,9 @@ eslint-visitor-keys@^1.0.0, eslint-visitor-keys@^1.1.0: integrity sha512-8y9YjtM1JBJU/A9Kc+SbaOV4y29sSWckBwMHa+FGtVj5gN/sbnKDf6xJUl+8g7FAij9LVaP8C24DUiH/f/2Z9A== eslint@^6.1.0: - version "6.2.2" - resolved "https://registry.yarnpkg.com/eslint/-/eslint-6.2.2.tgz#03298280e7750d81fcd31431f3d333e43d93f24f" - integrity sha512-mf0elOkxHbdyGX1IJEUsNBzCDdyoUgljF3rRlgfyYh0pwGnreLc0jjD6ZuleOibjmnUWZLY2eXwSooeOgGJ2jw== + version "6.5.1" + resolved "https://registry.yarnpkg.com/eslint/-/eslint-6.5.1.tgz#828e4c469697d43bb586144be152198b91e96ed6" + integrity sha512-32h99BoLYStT1iq1v2P9uwpyznQ4M2jRiFB6acitKz52Gqn+vPaMDUTB1bYi1WN4Nquj2w+t+bimYUG83DC55A== dependencies: "@babel/code-frame" "^7.0.0" ajv "^6.10.0" @@ -3861,12 +4002,12 @@ eslint@^6.1.0: v8-compile-cache "^2.0.3" espree@^6.1.1: - version "6.1.1" - resolved "https://registry.yarnpkg.com/espree/-/espree-6.1.1.tgz#7f80e5f7257fc47db450022d723e356daeb1e5de" - integrity sha512-EYbr8XZUhWbYCqQRW0duU5LxzL5bETN6AjKBGy1302qqzPaCH10QbRg3Wvco79Z8x9WbiE8HYB4e75xl6qUYvQ== + version "6.1.2" + resolved "https://registry.yarnpkg.com/espree/-/espree-6.1.2.tgz#6c272650932b4f91c3714e5e7b5f5e2ecf47262d" + integrity sha512-2iUPuuPP+yW1PZaMSDM9eyVf8D5P0Hi8h83YtZ5bPc/zHYjII5khoixIUTMO794NOY8F/ThF1Bo8ncZILarUTA== dependencies: - acorn "^7.0.0" - acorn-jsx "^5.0.2" + acorn "^7.1.0" + acorn-jsx "^5.1.0" eslint-visitor-keys "^1.1.0" esprima@^3.1.3: @@ -3908,10 +4049,10 @@ etag@~1.8.1: resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887" integrity sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc= -eventemitter3@^3.0.0: - version "3.1.2" - resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-3.1.2.tgz#2d3d48f9c346698fce83a85d7d664e98535df6e7" - integrity sha512-tvtQIeLVHjDkJYnzf2dgVMxfuSGJeM/7UCG17TT4EumTfNtF+0nebF/4zWOIkCreAbtNqhGEboB6BWrwqNaw4Q== +eventemitter3@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-4.0.0.tgz#d65176163887ee59f386d64c82610b696a4a74eb" + integrity sha512-qerSRB0p+UDEssxTtm6EDKcE7W4OaoisfIMl4CngyEhjpYglocpNg6UEqCvemdGhosAsg4sO2dXJOdyBifPGCg== events@^3.0.0: version "3.0.0" @@ -4180,6 +4321,13 @@ fill-range@^4.0.0: repeat-string "^1.6.1" to-regex-range "^2.1.0" +fill-range@^7.0.1: + version "7.0.1" + resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40" + integrity sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ== + dependencies: + to-regex-range "^5.0.1" + finalhandler@~1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.1.2.tgz#b7e7d000ffd11938d0fdb053506f6ebabe9f587d" @@ -4269,9 +4417,9 @@ flux@^3.1.3: fbjs "^0.8.0" follow-redirects@^1.0.0: - version "1.8.1" - resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.8.1.tgz#24804f9eaab67160b0e840c085885d606371a35b" - integrity sha512-micCIbldHioIegeKs41DoH0KS3AXfFzgS30qVkM6z/XOE/GJgvmsoc839NUqa1B9udYe9dQxgv7KFwng6+p/dw== + version "1.9.0" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.9.0.tgz#8d5bcdc65b7108fe1508649c79c12d732dcedb4f" + integrity sha512-CRcPzsSIbXyVDl0QI01muNDu69S8trU4jArW9LpOt2WtC6LyUJetcIrmfHsRBx7/Jb6GHJUiuqyYxPooFfNt6A== dependencies: debug "^3.0.0" @@ -4312,9 +4460,9 @@ fork-ts-checker-webpack-plugin@1.5.0: worker-rpc "^0.1.0" form-data@^2.3.1: - version "2.5.0" - resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.5.0.tgz#094ec359dc4b55e7d62e0db4acd76e89fe874d37" - integrity sha512-WXieX3G/8side6VIqx44ablyULoGruSde5PNTxoUyo5CeyAMX6nVWUd0rgist/EuX655cjhUhTo1Fo3tRYqbcA== + version "2.5.1" + resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.5.1.tgz#f2cbec57b5e59e23716e128fe44d4e5dd23895f4" + integrity sha512-m21N3WOmEEURgk6B9GLOE4RuWOFf28Lhh9qGYeNlGq4VDXUlJy2th2slBNU8Gp8EzloYZOibZJ7t5ecIrFSjVA== dependencies: asynckit "^0.4.0" combined-stream "^1.0.6" @@ -4377,12 +4525,21 @@ fs-extra@^4.0.2: jsonfile "^4.0.0" universalify "^0.1.0" +fs-extra@^8.1.0: + version "8.1.0" + resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-8.1.0.tgz#49d43c45a88cd9677668cb7be1b46efdb8d2e1c0" + integrity sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g== + dependencies: + graceful-fs "^4.2.0" + jsonfile "^4.0.0" + universalify "^0.1.0" + fs-minipass@^1.2.5: - version "1.2.6" - resolved "https://registry.yarnpkg.com/fs-minipass/-/fs-minipass-1.2.6.tgz#2c5cc30ded81282bfe8a0d7c7c1853ddeb102c07" - integrity sha512-crhvyXcMejjv3Z5d2Fa9sf5xLYVCF5O1c71QxbVnbLsmYMBEvDAftewesN/HhY03YRoA7zOMxjNGrF5svGaaeQ== + version "1.2.7" + resolved "https://registry.yarnpkg.com/fs-minipass/-/fs-minipass-1.2.7.tgz#ccff8570841e7fe4265693da88936c55aed7f7c7" + integrity sha512-GWSSJGFy4e9GUeCcbIkED+bgAoFyj7XF1mV8rma3QW4NIqX9Kyx79N/PF61H5udOV3aY1IaMLs6pGbH71nlCTA== dependencies: - minipass "^2.2.1" + minipass "^2.6.0" fs-write-stream-atomic@^1.0.8: version "1.0.10" @@ -4412,6 +4569,11 @@ fsevents@^1.2.7: nan "^2.12.1" node-pre-gyp "^0.12.0" +fsevents@~2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.1.1.tgz#74c64e21df71721845d0c44fe54b7f56b82995a9" + integrity sha512-4FRPXWETxtigtJW/gxzEDsX1LVbPAM93VleB83kZB+ellqbHMkyt2aJfuzNLRvFPnGi6bcE5SvfxgbXPeKteJw== + function-bind@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" @@ -4433,9 +4595,9 @@ functional-red-black-tree@^1.0.1: integrity sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc= functions-have-names@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/functions-have-names/-/functions-have-names-1.1.1.tgz#79d35927f07b8e7103d819fed475b64ccf7225ea" - integrity sha512-U0kNHUoxwPNPWOJaMG7Z00d4a/qZVrFtzWJRaK8V9goaVOCXBSQSJpt3MYGNtkScKEBKovxLjnNdC9MlXwo5Pw== + version "1.2.0" + resolved "https://registry.yarnpkg.com/functions-have-names/-/functions-have-names-1.2.0.tgz#83da7583e4ea0c9ac5ff530f73394b033e0bf77d" + integrity sha512-zKXyzksTeaCSw5wIX79iCA40YAa6CJMJgNg9wdkU/ERBrIdPSimPICYiLp65lRbSBqtiHql/HZfS2DyI/AH6tQ== gauge@~2.7.3: version "2.7.4" @@ -4462,9 +4624,9 @@ get-caller-file@^2.0.1: integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== get-own-enumerable-property-symbols@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/get-own-enumerable-property-symbols/-/get-own-enumerable-property-symbols-3.0.0.tgz#b877b49a5c16aefac3655f2ed2ea5b684df8d203" - integrity sha512-CIJYJC4GGF06TakLg8z4GQKvDsx9EMspVxOYih7LerEL/WosUnFIww45CGfxfeKHqlg3twgUrYRT1O3WQqjGCg== + version "3.0.1" + resolved "https://registry.yarnpkg.com/get-own-enumerable-property-symbols/-/get-own-enumerable-property-symbols-3.0.1.tgz#6f7764f88ea11e0b514bd9bd860a132259992ca4" + integrity sha512-09/VS4iek66Dh2bctjRkowueRJbY1JDGR1L/zRxO1Qk8Uxs6PnqaNSqalpizPT+CDjre3hnEsuzvhgomz9qYrA== get-stream@^4.0.0: version "4.1.0" @@ -4493,10 +4655,10 @@ glob-parent@^3.1.0: is-glob "^3.1.0" path-dirname "^1.0.0" -glob-parent@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.0.0.tgz#1dc99f0f39b006d3e92c2c284068382f0c20e954" - integrity sha512-Z2RwiujPRGluePM6j699ktJYxmPpJKCfpGA13jz2hmFZC7gKetzrWvg5KN3+OsIFmydGyZ1AVwERCq1w/ZZwRg== +glob-parent@^5.0.0, glob-parent@~5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.0.tgz#5f4c1d1e748d30cd73ad2944b3577a81b081e8c2" + integrity sha512-qjtRgnIVmOfnKUE3NJAQEdk+lKrxfw8t5ke7SXtfMTHcjsBfOfWXCQfdb30zfDoZQ2IRSIiidmjtbHZPZ++Ihw== dependencies: is-glob "^4.0.1" @@ -4562,7 +4724,7 @@ globby@^6.1.0: pify "^2.0.0" pinkie-promise "^2.0.0" -graceful-fs@^4.1.11, graceful-fs@^4.1.15, graceful-fs@^4.1.2, graceful-fs@^4.1.6: +graceful-fs@^4.1.11, graceful-fs@^4.1.15, graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.0: version "4.2.2" resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.2.tgz#6f0952605d0140c1cfdb138ed005775b92d67b02" integrity sha512-IItsdsea19BoLC7ELy13q1iJFNmd7ofZH5+X/pJr90/nRoPEX0DJo1dHDbgtYWOhJhcCgMDTOw84RZ72q6lB+Q== @@ -4591,9 +4753,9 @@ handle-thing@^2.0.0: integrity sha512-d4sze1JNC454Wdo2fkuyzCr6aHcbL6PGGuFAz0Li/NcOm1tCHGnWDRmJP85dh9IhQErTc2svWFEX5xHIOo//kQ== handlebars@^4.1.2: - version "4.1.2" - resolved "https://registry.yarnpkg.com/handlebars/-/handlebars-4.1.2.tgz#b6b37c1ced0306b221e094fc7aca3ec23b131b67" - integrity sha512-nvfrjqvt9xQ8Z/w0ijewdD/vvWDTOweBUm96NTr66Wfvo1mJenBLwcYmPs3TIBP5ruzYGD7Hx/DaM9RmhroGPw== + version "4.4.5" + resolved "https://registry.yarnpkg.com/handlebars/-/handlebars-4.4.5.tgz#1b1f94f9bfe7379adda86a8b73fb570265a0dddd" + integrity sha512-0Ce31oWVB7YidkaTq33ZxEbN+UDxMMgThvCe8ptgQViymL5DPis9uLdTA13MiRPhgvqyxIegugrP97iK3JeBHg== dependencies: neo-async "^2.6.0" optimist "^0.6.1" @@ -4705,17 +4867,22 @@ hex-color-regex@^1.1.0: resolved "https://registry.yarnpkg.com/hex-color-regex/-/hex-color-regex-1.1.0.tgz#4c06fccb4602fe2602b3c93df82d7e7dbf1a8a8e" integrity sha512-l9sfDFsuqtOqKDsQdqrMRk0U85RZc0RtOR9yPI7mRVOa4FsR/BVnZ0shmQRM96Ji99kYZP/7hn1cedc1+ApsTQ== +highlight-words-core@^1.2.0: + version "1.2.2" + resolved "https://registry.yarnpkg.com/highlight-words-core/-/highlight-words-core-1.2.2.tgz#1eff6d7d9f0a22f155042a00791237791b1eeaaa" + integrity sha512-BXUKIkUuh6cmmxzi5OIbUJxrG8OAk2MqoL1DtO3Wo9D2faJg2ph5ntyuQeLqaHJmzER6H5tllCDA9ZnNe9BVGg== + history@^4.9.0: - version "4.9.0" - resolved "https://registry.yarnpkg.com/history/-/history-4.9.0.tgz#84587c2068039ead8af769e9d6a6860a14fa1bca" - integrity sha512-H2DkjCjXf0Op9OAr6nJ56fcRkTSNrUiv41vNJ6IswJjif6wlpZK0BTfFbi7qK9dXLSYZxkq5lBsj3vUjlYBYZA== + version "4.10.1" + resolved "https://registry.yarnpkg.com/history/-/history-4.10.1.tgz#33371a65e3a83b267434e2b3f3b1b4c58aad4cf3" + integrity sha512-36nwAD620w12kuzPAsyINPWJqlNbij+hpK1k9XRloDtym8mxzGYl2c17LnV6IAGB2Dmg4tEa7G7DlawS0+qjew== dependencies: "@babel/runtime" "^7.1.2" loose-envify "^1.2.0" - resolve-pathname "^2.2.0" + resolve-pathname "^3.0.0" tiny-invariant "^1.0.2" tiny-warning "^1.0.0" - value-equal "^0.4.0" + value-equal "^1.0.1" hmac-drbg@^1.0.0: version "1.0.1" @@ -4734,9 +4901,9 @@ hoist-non-react-statics@^3.1.0, hoist-non-react-statics@^3.3.0: react-is "^16.7.0" hosted-git-info@^2.1.4: - version "2.8.4" - resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.8.4.tgz#44119abaf4bc64692a16ace34700fed9c03e2546" - integrity sha512-pzXIvANXEFrc5oFFXRMkbLPQ2rXRoDERwDLyrcUxGhaZhgP54BBSl9Oheh7Vv0T090cszWBxPjkQQ5Sq1PbBRQ== + version "2.8.5" + resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.8.5.tgz#759cfcf2c4d156ade59b0b2dfabddc42a6b9c70c" + integrity sha512-kssjab8CvdXfcXMXVcvsXum4Hwdq9XGtRD3TteMEvEbq0LXyiNQr6AprqKqfeaDXze7SxWvRxdpwE6ku7ikLkg== hpack.js@^2.1.6: version "2.1.6" @@ -4872,11 +5039,11 @@ http-proxy-middleware@^0.19.1: micromatch "^3.1.10" http-proxy@^1.17.0: - version "1.17.0" - resolved "https://registry.yarnpkg.com/http-proxy/-/http-proxy-1.17.0.tgz#7ad38494658f84605e2f6db4436df410f4e5be9a" - integrity sha512-Taqn+3nNvYRfJ3bGvKfBSRwy1v6eePlm3oc/aWVxZp57DQr5Eq3xhKJi7Z4hZpS8PC3H4qI+Yly5EmFacGuA/g== + version "1.18.0" + resolved "https://registry.yarnpkg.com/http-proxy/-/http-proxy-1.18.0.tgz#dbe55f63e75a347db7f3d99974f2692a314a6a3a" + integrity sha512-84I2iJM/n1d4Hdgc6y2+qY5mDaz2PUVjlg9znE9byl+q0uC3DeByqBGReQu5tpLK0TAqTIXScRUV+dg7+bUPpQ== dependencies: - eventemitter3 "^3.0.0" + eventemitter3 "^4.0.0" follow-redirects "^1.0.0" requires-port "^1.0.0" @@ -4948,9 +5115,9 @@ iferr@^0.1.5: integrity sha1-xg7taebY/bazEEofy8ocGS3FtQE= ignore-walk@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/ignore-walk/-/ignore-walk-3.0.1.tgz#a83e62e7d272ac0e3b551aaa82831a19b69f82f8" - integrity sha512-DTVlMx3IYPe0/JJcYP7Gxg7ttZZu3IInhuEhbchuqneY9wWe5Ojy2mXLBaQFUQmo0AW2r3qG7m1mg86js+gnlQ== + version "3.0.3" + resolved "https://registry.yarnpkg.com/ignore-walk/-/ignore-walk-3.0.3.tgz#017e2447184bfeade7c238e4aefdd1e8f95b1e37" + integrity sha512-m7o6xuOaT1aqheYHKf8W6J5pYH85ZI9w077erOzLje3JsB1gkafkAhHHY19dqjulgIZHFm32Cp5uNZgcQqdJKw== dependencies: minimatch "^3.0.4" @@ -5169,6 +5336,13 @@ is-binary-path@^1.0.0: dependencies: binary-extensions "^1.0.0" +is-binary-path@~2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09" + integrity sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw== + dependencies: + binary-extensions "^2.0.0" + is-boolean-object@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/is-boolean-object/-/is-boolean-object-1.0.0.tgz#98f8b28030684219a95f375cfbd88ce3405dff93" @@ -5286,7 +5460,7 @@ is-glob@^3.1.0: dependencies: is-extglob "^2.1.0" -is-glob@^4.0.0, is-glob@^4.0.1: +is-glob@^4.0.0, is-glob@^4.0.1, is-glob@~4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.1.tgz#7567dbe9f2f5e2467bc77ab83c4a29482407a5dc" integrity sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg== @@ -5305,6 +5479,11 @@ is-number@^3.0.0: dependencies: kind-of "^3.0.2" +is-number@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" + integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== + is-obj@^1.0.0, is-obj@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/is-obj/-/is-obj-1.0.1.tgz#3e4729ac1f5fde025cd7d83a896dab9f4f67db0f" @@ -5329,6 +5508,11 @@ is-path-inside@^1.0.0: dependencies: path-is-inside "^1.0.1" +is-plain-obj@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-1.1.0.tgz#71a50c8429dfca773c92a390a4a03b39fcd51d3e" + integrity sha1-caUMhCnfync8kqOQpKA7OfzVHT4= + is-plain-object@^2.0.1, is-plain-object@^2.0.3, is-plain-object@^2.0.4: version "2.0.4" resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-2.0.4.tgz#2c163b3fafb1b606d9d17928f05c2a1c38e07677" @@ -5501,7 +5685,7 @@ jest-changed-files@^24.9.0: execa "^1.0.0" throat "^4.0.0" -jest-cli@^24.8.0: +jest-cli@^24.9.0: version "24.9.0" resolved "https://registry.yarnpkg.com/jest-cli/-/jest-cli-24.9.0.tgz#ad2de62d07472d419c6abc301fc432b98b10d2af" integrity sha512-+VLRKyitT3BWoMeSUIHRxV/2g8y9gw91Jh5z2UmXZzkZKpbC08CSehVxgHUwTpy+HwGcns/tqafQDJW7imYvGg== @@ -5571,10 +5755,10 @@ jest-each@^24.9.0: jest-util "^24.9.0" pretty-format "^24.9.0" -jest-environment-enzyme@^7.1.0: - version "7.1.0" - resolved "https://registry.yarnpkg.com/jest-environment-enzyme/-/jest-environment-enzyme-7.1.0.tgz#1181d174034a2a4d8ba8fd6ba2123f243ccaf7ec" - integrity sha512-31nPBYx1MZfihsKUgZg16zLS4+f4gBvo4YpYMU4TIvQ2IjbREU9bLFwAszTgcs5mkE7SNOykm/68OjPmgDd4/Q== +jest-environment-enzyme@^7.1.1: + version "7.1.1" + resolved "https://registry.yarnpkg.com/jest-environment-enzyme/-/jest-environment-enzyme-7.1.1.tgz#d9dd7b6b72ed37d213ba77e05ab7f9062aa62402" + integrity sha512-k+QJkK0iRtjWbNfKdtj1QQIs12JbbvPmHW30cSbDoIgOFO7Bd1lLo6qOabM+PdhPCeLWQ1D1ZoTrHPauXdYpzA== dependencies: jest-environment-jsdom "^24.0.0" @@ -5610,14 +5794,14 @@ jest-environment-node@^24.9.0: jest-mock "^24.9.0" jest-util "^24.9.0" -jest-enzyme@^7.0.2: - version "7.1.0" - resolved "https://registry.yarnpkg.com/jest-enzyme/-/jest-enzyme-7.1.0.tgz#4345e989382a6cd70c15c7225dad9c9a9bb79cbc" - integrity sha512-ukL9jFwvQ3xbzhoKniDVTkAgZzGCFJNeJVU058RyY0R46fVhjJlPEV/hYlmJl518qfCmKNW6zXgtmyPcaDgxhA== +jest-enzyme@^7.1.1: + version "7.1.1" + resolved "https://registry.yarnpkg.com/jest-enzyme/-/jest-enzyme-7.1.1.tgz#7684720b795636503c7660e8c6f70bd644a485fd" + integrity sha512-ujMi/2OF16rsjsS2ozdZCukfRZGC/Sb3MoJjINXITTvZM6lTL14lDliJr1kYIlUZVrphw0fmZkTNVTP7DnJ+Xw== dependencies: - enzyme-matchers "^7.1.0" + enzyme-matchers "^7.1.1" enzyme-to-json "^3.3.0" - jest-environment-enzyme "^7.1.0" + jest-environment-enzyme "^7.1.1" jest-get-type@^24.9.0: version "24.9.0" @@ -5723,18 +5907,7 @@ jest-resolve-dependencies@^24.9.0: jest-regex-util "^24.3.0" jest-snapshot "^24.9.0" -jest-resolve@24.8.0: - version "24.8.0" - resolved "https://registry.yarnpkg.com/jest-resolve/-/jest-resolve-24.8.0.tgz#84b8e5408c1f6a11539793e2b5feb1b6e722439f" - integrity sha512-+hjSzi1PoRvnuOICoYd5V/KpIQmkAsfjFO71458hQ2Whi/yf1GDeBOFj8Gxw4LrApHsVJvn5fmjcPdmoUHaVKw== - dependencies: - "@jest/types" "^24.8.0" - browser-resolve "^1.11.3" - chalk "^2.0.1" - jest-pnp-resolver "^1.2.1" - realpath-native "^1.1.0" - -jest-resolve@^24.9.0: +jest-resolve@24.9.0, jest-resolve@^24.9.0: version "24.9.0" resolved "https://registry.yarnpkg.com/jest-resolve/-/jest-resolve-24.9.0.tgz#dff04c7687af34c4dd7e524892d9cf77e5d17321" integrity sha512-TaLeLVL1l08YFZAt3zaPtjiVvyy4oSA6CRe+0AFPPVX3Q/VI0giIWWoAvoS5L96vj9Dqxj4fB5p2qrHCmTU/MQ== @@ -5853,16 +6026,16 @@ jest-validate@^24.9.0: leven "^3.1.0" pretty-format "^24.9.0" -jest-watch-typeahead@0.3.1: - version "0.3.1" - resolved "https://registry.yarnpkg.com/jest-watch-typeahead/-/jest-watch-typeahead-0.3.1.tgz#47701024b64b444aa325d801b4b3a6d61ed70701" - integrity sha512-cDIko96c4Yqg/7mfye1eEYZ6Pvugo9mnOOhGQod3Es7/KptNv1b+9gFVaotzdqNqTlwbkA80BnWHtzV4dc+trA== +jest-watch-typeahead@0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/jest-watch-typeahead/-/jest-watch-typeahead-0.4.0.tgz#4d5356839a85421588ce452d2440bf0d25308397" + integrity sha512-bJR/HPNgOQnkmttg1OkBIrYFAYuxFxExtgQh67N2qPvaWGVC8TCkedRNPKBfmZfVXFD3u2sCH+9OuS5ApBfCgA== dependencies: - ansi-escapes "^3.0.0" + ansi-escapes "^4.2.1" chalk "^2.4.1" jest-watcher "^24.3.0" - slash "^2.0.0" - string-length "^2.0.0" + slash "^3.0.0" + string-length "^3.1.0" strip-ansi "^5.0.0" jest-watcher@^24.3.0, jest-watcher@^24.9.0: @@ -5886,13 +6059,13 @@ jest-worker@^24.6.0, jest-worker@^24.9.0: merge-stream "^2.0.0" supports-color "^6.1.0" -jest@24.8.0: - version "24.8.0" - resolved "https://registry.yarnpkg.com/jest/-/jest-24.8.0.tgz#d5dff1984d0d1002196e9b7f12f75af1b2809081" - integrity sha512-o0HM90RKFRNWmAWvlyV8i5jGZ97pFwkeVoGvPW1EtLTgJc2+jcuqcbbqcSZLE/3f2S5pt0y2ZBETuhpWNl1Reg== +jest@24.9.0: + version "24.9.0" + resolved "https://registry.yarnpkg.com/jest/-/jest-24.9.0.tgz#987d290c05a08b52c56188c1002e368edb007171" + integrity sha512-YvkBL1Zm7d2B1+h5fHEOdyjCG+sGMz4f8D86/0HiqJ6MB4MnDc8FgP5vdWsGnemOQro7lnYo8UakZ3+5A0jxGw== dependencies: import-local "^2.0.0" - jest-cli "^24.8.0" + jest-cli "^24.9.0" jquery@^3.3.1: version "3.4.1" @@ -6051,9 +6224,9 @@ json5@^1.0.1: minimist "^1.2.0" json5@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/json5/-/json5-2.1.0.tgz#e7a0c62c48285c628d20a10b85c89bb807c32850" - integrity sha512-8Mh9h6xViijj36g7Dxi+Y4S6hNGV96vcJZr/SrlHh1LR/pEn/8j/+qIBbs44YKl69Lrfctp4QD+AdWLTMqEZAQ== + version "2.1.1" + resolved "https://registry.yarnpkg.com/json5/-/json5-2.1.1.tgz#81b6cb04e9ba496f1c7005d07b4368a2638f90b6" + integrity sha512-l+3HXD0GEI3huGq1njuqtzYK8OYJyXMkOLtQ53pjWh89tvWS2h6l+1zMkYWqlb57+SiQodKZyvMEFb2X+KrFhQ== dependencies: minimist "^1.2.0" @@ -6196,7 +6369,7 @@ load-json-file@^4.0.0: pify "^3.0.0" strip-bom "^3.0.0" -loader-fs-cache@^1.0.0: +loader-fs-cache@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/loader-fs-cache/-/loader-fs-cache-1.0.2.tgz#54cedf6b727e1779fd8f01205f05f6e88706f086" integrity sha512-70IzT/0/L+M20jUlEqZhZyArTU6VKLRTYRDAYN26g4jfzpJqjipLL3/hgYpySqI9PwsVRHHFja0LfEmsx9X2Cw== @@ -6301,16 +6474,6 @@ lodash.isequal@^4.5.0: resolved "https://registry.yarnpkg.com/lodash.isequal/-/lodash.isequal-4.5.0.tgz#415c4478f2bcc30120c22ce10ed3226f7d3e18e0" integrity sha1-QVxEePK8wwEgwizhDtMib30+GOA= -lodash.isfunction@^3.0.9: - version "3.0.9" - resolved "https://registry.yarnpkg.com/lodash.isfunction/-/lodash.isfunction-3.0.9.tgz#06de25df4db327ac931981d1bdb067e5af68d051" - integrity sha512-AirXNj15uRIMMPihnkInB4i3NHeb4iBtNg9WRWuK2o31S+ePwwNmDPaTL3o7dTJ+VXNZim7rFs4rxN4YU1oUJw== - -lodash.isobject@^3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/lodash.isobject/-/lodash.isobject-3.0.2.tgz#3c8fb8d5b5bf4bf90ae06e14f2a530a4ed935e1d" - integrity sha1-PI+41bW/S/kK4G4U8qUwpO2TXh0= - lodash.istypedarray@^3.0.0: version "3.0.6" resolved "https://registry.yarnpkg.com/lodash.istypedarray/-/lodash.istypedarray-3.0.6.tgz#c9a477498607501d8e8494d283b87c39281cef62" @@ -6350,11 +6513,6 @@ lodash.templatesettings@^4.0.0: dependencies: lodash._reinterpolate "^3.0.0" -lodash.tonumber@^4.0.3: - version "4.0.3" - resolved "https://registry.yarnpkg.com/lodash.tonumber/-/lodash.tonumber-4.0.3.tgz#0b96b31b35672793eb7f5a63ee791f1b9e9025d9" - integrity sha1-C5azGzVnJ5Prf1pj7nkfG56QJdk= - lodash.unescape@4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/lodash.unescape/-/lodash.unescape-4.0.1.tgz#bf2249886ce514cda112fae9218cdc065211fc9c" @@ -6371,9 +6529,9 @@ lodash.uniq@^4.5.0: integrity sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A== loglevel@^1.4.1: - version "1.6.3" - resolved "https://registry.yarnpkg.com/loglevel/-/loglevel-1.6.3.tgz#77f2eb64be55a404c9fd04ad16d57c1d6d6b1280" - integrity sha512-LoEDv5pgpvWgPF4kNYuIp0qqSJVWak/dML0RY74xlzMZiT9w77teNAwKYKWBTYjlokMirg+o3jBwp+vlLrcfAA== + version "1.6.4" + resolved "https://registry.yarnpkg.com/loglevel/-/loglevel-1.6.4.tgz#f408f4f006db8354d0577dcf6d33485b3cb90d56" + integrity sha512-p0b6mOGKcGa+7nnmKbpzR6qloPbrgLcnio++E+14Vo/XffOGwZtRpUhr8dTH/x2oCMmEoIU0Zwm3ZauhvYD17g== loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.2.0, loose-envify@^1.3.1, loose-envify@^1.4.0: version "1.4.0" @@ -6466,7 +6624,12 @@ mem@^4.0.0: mimic-fn "^2.0.0" p-is-promise "^2.0.0" -memory-fs@^0.4.0, memory-fs@^0.4.1: +memoize-one@^4.0.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/memoize-one/-/memoize-one-4.1.0.tgz#a2387c58c03fff27ca390c31b764a79addf3f906" + integrity sha512-2GApq0yI/b22J2j9rhbrAlsHb0Qcz+7yWxeLG8h+95sl1XPUgeLimQSOdur4Vw7cUhrBHwaUZxWFZueojqNRzA== + +memory-fs@^0.4.1: version "0.4.1" resolved "https://registry.yarnpkg.com/memory-fs/-/memory-fs-0.4.1.tgz#3a9a20b8462523e447cfbc7e8bb80ed667bfc552" integrity sha1-OpoguEYlI+RHz7x+i7gO1me/xVI= @@ -6474,6 +6637,14 @@ memory-fs@^0.4.0, memory-fs@^0.4.1: errno "^0.1.3" readable-stream "^2.0.1" +memory-fs@^0.5.0: + version "0.5.0" + resolved "https://registry.yarnpkg.com/memory-fs/-/memory-fs-0.5.0.tgz#324c01288b88652966d161db77838720845a8e3c" + integrity sha512-jA0rdU5KoQMC0e6ppoNRtpp6vjFq6+NY7r8hywnC7V+1Xj/MtHwGIbB1QaK/dunyjWteJzmkpd7ooeWg10T7GA== + dependencies: + errno "^0.1.3" + readable-stream "^2.0.1" + merge-deep@^3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/merge-deep/-/merge-deep-3.0.2.tgz#f39fa100a4f1bd34ff29f7d2bf4508fbb8d83ad2" @@ -6494,9 +6665,9 @@ merge-stream@^2.0.0: integrity sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w== merge2@^1.2.3: - version "1.2.4" - resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.2.4.tgz#c9269589e6885a60cf80605d9522d4b67ca646e3" - integrity sha512-FYE8xI+6pjFOhokZu0We3S5NKCirLbCzSh2Usf3qEyr4X8U+0jNg9P8RZ4qz+V2UoECLVwSyzU3LxXBaLGtD3A== + version "1.3.0" + resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.3.0.tgz#5b366ee83b2f1582c48f87e47cf1a9352103ca81" + integrity sha512-2j4DAdlBOkiSZIsaXk4mTE3sRS02yBHAtfy127xRV3bQUFqXkjHCHLW6Scv7DwNRbIWNHH8zpnz9zMaKXIdvYw== methods@^1.1.1, methods@~1.1.2: version "1.1.2" @@ -6535,11 +6706,16 @@ miller-rabin@^4.0.0: bn.js "^4.0.0" brorand "^1.0.1" -mime-db@1.40.0, "mime-db@>= 1.40.0 < 2": +mime-db@1.40.0: version "1.40.0" resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.40.0.tgz#a65057e998db090f732a68f6c276d387d4126c32" integrity sha512-jYdeOMPy9vnxEqFRRo6ZvTZ8d9oPb+k18PKoYNYUe2stVEBPPwsln/qWzdbmaIvnhZ9v2P+CuecK+fpUfsV2mA== +"mime-db@>= 1.40.0 < 2": + version "1.42.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.42.0.tgz#3e252907b4c7adb906597b4b65636272cf9e7bac" + integrity sha512-UbfJCR4UAVRNgMpfImz05smAXK7+c+ZntjaA26ANtkXLlOe947Aag5zdIcKQULAiF9Cq4WxBi9jUs5zkA84bYQ== + mime-types@^2.1.12, mime-types@~2.1.17, mime-types@~2.1.19, mime-types@~2.1.24: version "2.1.24" resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.24.tgz#b6f8d0b3e951efb77dedeca194cff6d16f676f81" @@ -6552,7 +6728,7 @@ mime@1.6.0, mime@^1.4.1: resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1" integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg== -mime@^2.4.2, mime@^2.4.4: +mime@^2.4.4: version "2.4.4" resolved "https://registry.yarnpkg.com/mime/-/mime-2.4.4.tgz#bd7b91135fc6b01cde3e9bae33d659b63d8857e5" integrity sha512-LRxmNwziLPT828z+4YkNzloCFC2YM4wrB99k+AV5ZbEyfGNWfG8SO1FUXLmLDBSo89NrJZ4DIWeLjy1CHGhMGA== @@ -6576,12 +6752,13 @@ mini-create-react-context@^0.3.0: gud "^1.0.0" tiny-warning "^1.0.2" -mini-css-extract-plugin@0.5.0: - version "0.5.0" - resolved "https://registry.yarnpkg.com/mini-css-extract-plugin/-/mini-css-extract-plugin-0.5.0.tgz#ac0059b02b9692515a637115b0cc9fed3a35c7b0" - integrity sha512-IuaLjruM0vMKhUUT51fQdQzBYTX49dLj8w68ALEAe2A4iYNpIC4eMac67mt3NzycvjOlf07/kYxJDc0RTl1Wqw== +mini-css-extract-plugin@0.8.0: + version "0.8.0" + resolved "https://registry.yarnpkg.com/mini-css-extract-plugin/-/mini-css-extract-plugin-0.8.0.tgz#81d41ec4fe58c713a96ad7c723cdb2d0bd4d70e1" + integrity sha512-MNpRGbNA52q6U92i0qbVpQNsgk7LExy41MdAlG84FeytfDOtRIf/mCHdEgG8rpTKOaNKiqUnZdlptF469hxqOw== dependencies: loader-utils "^1.1.0" + normalize-url "1.9.1" schema-utils "^1.0.0" webpack-sources "^1.1.0" @@ -6617,20 +6794,20 @@ minimist@~0.0.1: resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.10.tgz#de3f98543dbf96082be48ad1a0c7cda836301dcf" integrity sha1-3j+YVD2/lggr5IrRoMfNqDYwHc8= -minipass@^2.2.1, minipass@^2.3.5: - version "2.4.0" - resolved "https://registry.yarnpkg.com/minipass/-/minipass-2.4.0.tgz#38f0af94f42fb6f34d3d7d82a90e2c99cd3ff485" - integrity sha512-6PmOuSP4NnZXzs2z6rbwzLJu/c5gdzYg1mRI/WIYdx45iiX7T+a4esOzavD6V/KmBzAaopFSTZPZcUx73bqKWA== +minipass@^2.6.0, minipass@^2.8.6, minipass@^2.9.0: + version "2.9.0" + resolved "https://registry.yarnpkg.com/minipass/-/minipass-2.9.0.tgz#e713762e7d3e32fed803115cf93e04bca9fcc9a6" + integrity sha512-wxfUjg9WebH+CUDX/CdbRlh5SmfZiy/hpkxaRI16Y9W56Pa75sWgd/rvFilSgrauD9NyFymP/+JFV3KwzIsJeg== dependencies: safe-buffer "^5.1.2" yallist "^3.0.0" minizlib@^1.2.1: - version "1.2.1" - resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-1.2.1.tgz#dd27ea6136243c7c880684e8672bb3a45fd9b614" - integrity sha512-7+4oTUOWKg7AuL3vloEWekXY2/D20cevzsrNT2kGWm+39J9hGTCBv8VI5Pm5lXZ/o3/mdR4f8rflAPhnQb8mPA== + version "1.3.3" + resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-1.3.3.tgz#2290de96818a34c29551c8a8d301216bd65a861d" + integrity sha512-6ZYMOEnmVsdCeTJVE0W9ZD+pVnE8h9Hma/iOwwRDsdQoePpoX56/8B6z3P9VNwppJuBKNRuFDRNRqRWexT9G9Q== dependencies: - minipass "^2.2.1" + minipass "^2.9.0" mississippi@^3.0.0: version "3.0.0" @@ -6664,7 +6841,7 @@ mixin-object@^2.0.1: for-in "^0.1.3" is-extendable "^0.1.1" -mkdirp@0.5.1, mkdirp@0.5.x, mkdirp@^0.5.0, mkdirp@^0.5.1, mkdirp@~0.5.1: +mkdirp@0.5.1, mkdirp@^0.5.0, mkdirp@^0.5.1, mkdirp@~0.5.1: version "0.5.1" resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903" integrity sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM= @@ -6749,9 +6926,9 @@ natural-compare@^1.4.0: integrity sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc= nearley@^2.7.10: - version "2.18.0" - resolved "https://registry.yarnpkg.com/nearley/-/nearley-2.18.0.tgz#a9193612dd6d528a2e47e743b1dc694cfe105223" - integrity sha512-/zQOMCeJcioI0xJtd5RpBiWw2WP7wLe6vq8/3Yu0rEwgus/G/+pViX80oA87JdVgjRt2895mZSv2VfZmy4W1uw== + version "2.19.0" + resolved "https://registry.yarnpkg.com/nearley/-/nearley-2.19.0.tgz#37717781d0fd0f2bfc95e233ebd75678ca4bda46" + integrity sha512-2v52FTw7RPqieZr3Gth1luAXZR7Je6q3KaDHY5bjl/paDUdMu35fZ8ICNgiYJRr3tf3NMvIQQR1r27AvEr9CRA== dependencies: commander "^2.19.0" moo "^0.4.3" @@ -6803,10 +6980,10 @@ node-fetch@^1.0.1: encoding "^0.1.11" is-stream "^1.0.1" -node-forge@0.7.5: - version "0.7.5" - resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-0.7.5.tgz#6c152c345ce11c52f465c2abd957e8639cd674df" - integrity sha512-MmbQJ2MTESTjt3Gi/3yG1wGpIMhUfcIypUCGtTizFR9IiccFwxSpfp0vtIZlkFclEqERemxfnSdZEMR9VqqEFQ== +node-forge@0.9.0: + version "0.9.0" + resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-0.9.0.tgz#d624050edbb44874adca12bb9a52ec63cb782579" + integrity sha512-7ASaDa3pD+lJ3WvXFsxekJQelBKRpne+GOVbLbtHYdd7pFspyeuJHnWfLplGf3SwKGbfs/aYl5V/JCIaHVUKKQ== node-int64@^0.4.0: version "0.4.0" @@ -6874,12 +7051,12 @@ node-pre-gyp@^0.12.0: semver "^5.3.0" tar "^4" -node-releases@^1.1.25: - version "1.1.28" - resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-1.1.28.tgz#503c3c70d0e4732b84e7aaa2925fbdde10482d4a" - integrity sha512-AQw4emh6iSXnCpDiFe0phYcThiccmkNWMZnFZ+lDJjAP8J0m2fVd59duvUUyuTirQOhIAajTFkzG6FHCLBO59g== +node-releases@^1.1.29, node-releases@^1.1.36: + version "1.1.36" + resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-1.1.36.tgz#44b7cb8254138e87bdbfa47761d0f825e20900b4" + integrity sha512-ggXhX6QGyJSjj3r+6ml2LqqC28XOWmKtpb+a15/Zpr9V3yoNazxJNlcQDS9bYaid5FReEWHEgToH1mwoUceWwg== dependencies: - semver "^5.3.0" + semver "^6.3.0" nopt@^4.0.1: version "4.0.1" @@ -6906,7 +7083,7 @@ normalize-path@^2.1.1: dependencies: remove-trailing-separator "^1.0.1" -normalize-path@^3.0.0: +normalize-path@^3.0.0, normalize-path@~3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== @@ -6916,6 +7093,16 @@ normalize-range@^0.1.2: resolved "https://registry.yarnpkg.com/normalize-range/-/normalize-range-0.1.2.tgz#2d10c06bdfd312ea9777695a4d28439456b75942" integrity sha1-LRDAa9/TEuqXd2laTShDlFa3WUI= +normalize-url@1.9.1: + version "1.9.1" + resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-1.9.1.tgz#2cc0d66b31ea23036458436e3620d85954c66c3c" + integrity sha1-LMDWazHqIwNkWENuNiDYWVTGbDw= + dependencies: + object-assign "^4.0.1" + prepend-http "^1.0.0" + query-string "^4.1.0" + sort-keys "^1.0.0" + normalize-url@^3.0.0: version "3.3.0" resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-3.3.0.tgz#b2e1c4dc4f7c6d57743df733a4f5978d18650559" @@ -6932,9 +7119,9 @@ npm-bundled@^1.0.1: integrity sha512-8/JCaftHwbd//k6y2rEWp6k1wxVfpFzB6t1p825+cUb7Ym2XQfhwIC5KwhrvzZRJu+LtDE585zVaS32+CGtf0g== npm-packlist@^1.1.6: - version "1.4.4" - resolved "https://registry.yarnpkg.com/npm-packlist/-/npm-packlist-1.4.4.tgz#866224233850ac534b63d1a6e76050092b5d2f44" - integrity sha512-zTLo8UcVYtDU3gdeaFu2Xu0n0EvelfHDGuqtNIn5RO7yQj4H1TqNdBc/yZjxnWA0PVB8D3Woyp0i5B43JwQ6Vw== + version "1.4.6" + resolved "https://registry.yarnpkg.com/npm-packlist/-/npm-packlist-1.4.6.tgz#53ba3ed11f8523079f1457376dd379ee4ea42ff4" + integrity sha512-u65uQdb+qwtGvEJh/DgQgW1Xg7sqeNbmxYyrvlNznaVTjV3E5P6F/EFjM+BVHXl7JJlsdG8A64M0XI8FI/IOlg== dependencies: ignore-walk "^3.0.1" npm-bundled "^1.0.1" @@ -6997,7 +7184,7 @@ object-copy@^0.1.0: define-property "^0.2.5" kind-of "^3.0.3" -object-hash@^1.1.4: +object-hash@^1.3.1: version "1.3.1" resolved "https://registry.yarnpkg.com/object-hash/-/object-hash-1.3.1.tgz#fde452098a951cb145f039bb7d455449ddc126df" integrity sha512-OSuu/pU4ENM9kmREg0BdNrUDIl1heYa4mBZacJc+vVWz4GtAwu7jO8s4AIt2aGRUTqxykpWzI3Oqnsm13tTMDA== @@ -7049,15 +7236,15 @@ object.entries@^1.0.4, object.entries@^1.1.0: function-bind "^1.1.1" has "^1.0.3" -object.fromentries@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/object.fromentries/-/object.fromentries-2.0.0.tgz#49a543d92151f8277b3ac9600f1e930b189d30ab" - integrity sha512-9iLiI6H083uiqUuvzyY6qrlmc/Gz8hLQFOcb/Ri/0xXFkSNS3ctV+CbE6yM2+AnkYfOB3dGjdzC0wrMLIhQICA== +object.fromentries@^2.0.0, object.fromentries@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/object.fromentries/-/object.fromentries-2.0.1.tgz#050f077855c7af8ae6649f45c80b16ee2d31e704" + integrity sha512-PUQv8Hbg3j2QX0IQYv3iAGCbGcu4yY4KQ92/dhA4sFSixBmSmp13UpDLs6jGK8rBtbmhNNIK99LD2k293jpiGA== dependencies: - define-properties "^1.1.2" - es-abstract "^1.11.0" + define-properties "^1.1.3" + es-abstract "^1.15.0" function-bind "^1.1.1" - has "^1.0.1" + has "^1.0.3" object.getownpropertydescriptors@^2.0.3: version "2.0.3" @@ -7272,11 +7459,11 @@ pako@~1.0.5: integrity sha512-0DTvPVU3ed8+HNXOu5Bs+o//Mbdj9VNQMUOe9oKCwh8l0GNwpTDMKCWbRjgtD291AWnkAgkqA/LOnQS8AmS1tw== parallel-transform@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/parallel-transform/-/parallel-transform-1.1.0.tgz#d410f065b05da23081fcd10f28854c29bda33b06" - integrity sha1-1BDwZbBdojCB/NEPKIVMKb2jOwY= + version "1.2.0" + resolved "https://registry.yarnpkg.com/parallel-transform/-/parallel-transform-1.2.0.tgz#9049ca37d6cb2182c3b1d2c720be94d14a5814fc" + integrity sha512-P2vSmIu38uIlvdcU7fDkyrxj33gTUy/ABO5ZUbGowxNCopBq/OoD42bP4UmMrJoPyk4Uqf0mu3mtWBhHCZD8yg== dependencies: - cyclist "~0.2.2" + cyclist "^1.0.1" inherits "^2.0.3" readable-stream "^2.1.5" @@ -7295,9 +7482,9 @@ parent-module@^1.0.0: callsites "^3.0.0" parse-asn1@^5.0.0: - version "5.1.4" - resolved "https://registry.yarnpkg.com/parse-asn1/-/parse-asn1-5.1.4.tgz#37f6628f823fbdeb2273b4d540434a22f3ef1fcc" - integrity sha512-Qs5duJcuvNExRfFZ99HDD3z4mAi3r9Wl/FOjEOijlxwCZs7E7mW2vjTpgQ4J8LpTF8x5v+1Vn5UQFejmWT11aw== + version "5.1.5" + resolved "https://registry.yarnpkg.com/parse-asn1/-/parse-asn1-5.1.5.tgz#003271343da58dc94cace494faef3d2147ecea0e" + integrity sha512-jkMYn1dcJqF6d5CpU689bq7w/b5ALS9ROVSpQDPrZsqqesUJii9qutvoT5ltGedNXMO2e16YUWIghG9KxaViTQ== dependencies: asn1.js "^4.0.0" browserify-aes "^1.0.0" @@ -7432,6 +7619,11 @@ performance-now@^2.1.0: resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b" integrity sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns= +picomatch@^2.0.4: + version "2.0.7" + resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.0.7.tgz#514169d8c7cd0bdbeecc8a2609e34a7163de69f6" + integrity sha512-oLHIdio3tZ0qH76NybpeneBhYVj0QFTfXEFTc/B3zKQspYfYYkWYgFsmzo+4kvId/bQRcNkVeguI3y+CD22BtA== + pify@^2.0.0: version "2.3.0" resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c" @@ -7506,19 +7698,19 @@ pnp-webpack-plugin@1.5.0: dependencies: ts-pnp "^1.1.2" -popper.js@^1.14.4: - version "1.15.0" - resolved "https://registry.yarnpkg.com/popper.js/-/popper.js-1.15.0.tgz#5560b99bbad7647e9faa475c6b8056621f5a4ff2" - integrity sha512-w010cY1oCUmI+9KwwlWki+r5jxKfTFDVoadl7MSrIujHU5MJ5OR6HTDj6Xo8aoR/QsA56x8jKjA59qGH4ELtrA== +popper.js@^1.14.4, popper.js@^1.16.0: + version "1.16.0" + resolved "https://registry.yarnpkg.com/popper.js/-/popper.js-1.16.0.tgz#2e1816bcbbaa518ea6c2e15a466f4cb9c6e2fbb3" + integrity sha512-+G+EkOPoE5S/zChTpmBSSDYmhXJ5PsW8eMhH8cP/CQHMFPBG/kC9Y5IIw6qNYgdJ+/COf0ddY2li28iHaZRSjw== portfinder@^1.0.9: - version "1.0.23" - resolved "https://registry.yarnpkg.com/portfinder/-/portfinder-1.0.23.tgz#894db4bcc5daf02b6614517ce89cd21a38226b82" - integrity sha512-B729mL/uLklxtxuiJKfQ84WPxNw5a7Yhx3geQZdcA4GjNjZSTSSMMWyoennMVnTWSmAR0lMdzWYN0JLnHrg1KQ== + version "1.0.25" + resolved "https://registry.yarnpkg.com/portfinder/-/portfinder-1.0.25.tgz#254fd337ffba869f4b9d37edc298059cb4d35eca" + integrity sha512-6ElJnHBbxVA1XSLgBp7G1FiCkQdlqGzuF7DswL5tcea+E8UpuvPU7beVAjjRwCioTS9ZluNbu+ZyRvgTsmqEBg== dependencies: - async "^1.5.2" - debug "^2.2.0" - mkdirp "0.5.x" + async "^2.6.2" + debug "^3.1.1" + mkdirp "^0.5.1" posix-character-classes@^0.1.0: version "0.1.1" @@ -8153,7 +8345,7 @@ postcss-value-parser@^3.0.0, postcss-value-parser@^3.3.0, postcss-value-parser@^ resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz#9ff822547e2893213cf1c30efa51ac5fd1ba8281" integrity sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ== -postcss-value-parser@^4.0.0: +postcss-value-parser@^4.0.2: version "4.0.2" resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.0.2.tgz#482282c09a42706d1fc9a069b73f44ec08391dc9" integrity sha512-LmeoohTpp/K4UiyQCwuGWlONxXamGzCMtFxLq4W1nZVGIQLYvMCJx3yAF9qyyuFpflABI9yVdtJAqbihOsCsJQ== @@ -8176,10 +8368,10 @@ postcss@7.0.14: source-map "^0.6.1" supports-color "^6.1.0" -postcss@^7.0.0, postcss@^7.0.1, postcss@^7.0.14, postcss@^7.0.17, postcss@^7.0.2, postcss@^7.0.5, postcss@^7.0.6: - version "7.0.17" - resolved "https://registry.yarnpkg.com/postcss/-/postcss-7.0.17.tgz#4da1bdff5322d4a0acaab4d87f3e782436bad31f" - integrity sha512-546ZowA+KZ3OasvQZHsbuEpysvwTZNGJv9EfyCQdsIDltPSWHAeTQ5fQy/Npi2ZDtLI3zs7Ps/p6wThErhm9fQ== +postcss@^7.0.0, postcss@^7.0.1, postcss@^7.0.14, postcss@^7.0.17, postcss@^7.0.18, postcss@^7.0.2, postcss@^7.0.5, postcss@^7.0.6: + version "7.0.18" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-7.0.18.tgz#4b9cda95ae6c069c67a4d933029eddd4838ac233" + integrity sha512-/7g1QXXgegpF+9GJj4iN7ChGF40sYuGYJ8WZu8DZWnmhQ/G36hfdk3q9LBJmoK+lZ+yzZ5KYpOoxq7LF1BxE8g== dependencies: chalk "^2.4.2" source-map "^0.6.1" @@ -8190,6 +8382,11 @@ prelude-ls@~1.1.2: resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54" integrity sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ= +prepend-http@^1.0.0: + version "1.0.4" + resolved "https://registry.yarnpkg.com/prepend-http/-/prepend-http-1.0.4.tgz#d4f4562b0ce3696e41ac52d0e002e57a635dc6dc" + integrity sha1-1PRWKwzjaW5BrFLQ4ALlemNdxtw= + pretty-bytes@^5.1.0: version "5.3.0" resolved "https://registry.yarnpkg.com/pretty-bytes/-/pretty-bytes-5.3.0.tgz#f2849e27db79fb4d6cfe24764fc4134f165989f2" @@ -8297,9 +8494,9 @@ prr@~1.0.1: integrity sha1-0/wRS6BplaRexok/SEzrHXj19HY= psl@^1.1.24, psl@^1.1.28: - version "1.3.0" - resolved "https://registry.yarnpkg.com/psl/-/psl-1.3.0.tgz#e1ebf6a3b5564fa8376f3da2275da76d875ca1bd" - integrity sha512-avHdspHO+9rQTLbv1RO+MPYeP/SzsCoxofjVnHanETfQhTJrmB0HlDoW+EiN/R+C0BZ+gERab9NY0lPN2TxNag== + version "1.4.0" + resolved "https://registry.yarnpkg.com/psl/-/psl-1.4.0.tgz#5dd26156cdb69fa1fdb8ab1991667d3f80ced7c2" + integrity sha512-HZzqCGPecFLyoRj5HLfuDSKYTJkAfB5thKBIkRHtGjWwY7p1dAyveIbXIq4tO0KYfDF2tHqPUgY9SDnGm00uFw== public-encrypt@^4.0.0: version "4.0.3" @@ -8369,15 +8566,23 @@ qs@6.7.0: integrity sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ== qs@^6.5.1, qs@^6.7.0: - version "6.8.0" - resolved "https://registry.yarnpkg.com/qs/-/qs-6.8.0.tgz#87b763f0d37ca54200334cd57bb2ef8f68a1d081" - integrity sha512-tPSkj8y92PfZVbinY1n84i1Qdx75lZjMQYx9WZhnkofyxzw2r7Ho39G3/aEvSUdebxpnnM4LZJCtvE/Aq3+s9w== + version "6.9.0" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.9.0.tgz#d1297e2a049c53119cb49cca366adbbacc80b409" + integrity sha512-27RP4UotQORTpmNQDX8BHPukOnBP3p1uUJY5UnDhaJB+rMt9iMsok724XL+UHU23bEFOHRMQ2ZhI99qOWUMGFA== qs@~6.5.2: version "6.5.2" resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36" integrity sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA== +query-string@^4.1.0: + version "4.3.4" + resolved "https://registry.yarnpkg.com/query-string/-/query-string-4.3.4.tgz#bbb693b9ca915c232515b228b1a02b609043dbeb" + integrity sha1-u7aTucqRXCMlFbIosaArYJBD2+s= + dependencies: + object-assign "^4.1.0" + strict-uri-encode "^1.0.0" + querystring-es3@^0.2.0: version "0.2.1" resolved "https://registry.yarnpkg.com/querystring-es3/-/querystring-es3-0.2.1.tgz#9ec61f79049875707d69414596fd907a4d711e73" @@ -8453,12 +8658,12 @@ rc@^1.2.7: minimist "^1.2.0" strip-json-comments "~2.0.1" -react-app-polyfill@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/react-app-polyfill/-/react-app-polyfill-1.0.2.tgz#2a51175885c88245a2a356dc46df29f38ec9f060" - integrity sha512-yZcpLnIr0FOIzrOOz9JC37NWAWEuCaQWmYn9EWjEzlCW4cOmA5MkT5L3iP8QuUeFnoqVCTJgjIWYbXEJgNXhGA== +react-app-polyfill@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/react-app-polyfill/-/react-app-polyfill-1.0.4.tgz#4dd2636846b585c2d842b1e44e1bc29044345874" + integrity sha512-5Vte6ki7jpNsNCUKaboyofAhmURmCn2Y6Hu7ydJ6Iu4dct1CIGoh/1FT7gUZKAbowVX2lxVPlijvp1nKxfAl4w== dependencies: - core-js "3.1.4" + core-js "3.2.1" object-assign "4.1.1" promise "8.0.3" raf "3.4.1" @@ -8475,14 +8680,14 @@ react-base16-styling@^0.6.0: lodash.flow "^3.3.0" pure-color "^1.2.0" -react-dev-utils@^9.0.3: - version "9.0.3" - resolved "https://registry.yarnpkg.com/react-dev-utils/-/react-dev-utils-9.0.3.tgz#7607455587abb84599451460eb37cef0b684131a" - integrity sha512-OyInhcwsvycQ3Zr2pQN+HV4gtRXrky5mJXIy4HnqrWa+mI624xfYfqGuC9dYbxp4Qq3YZzP8GSGQjv0AgNU15w== +react-dev-utils@^9.1.0: + version "9.1.0" + resolved "https://registry.yarnpkg.com/react-dev-utils/-/react-dev-utils-9.1.0.tgz#3ad2bb8848a32319d760d0a84c56c14bdaae5e81" + integrity sha512-X2KYF/lIGyGwP/F/oXgGDF24nxDA2KC4b7AFto+eqzc/t838gpSGiaU8trTqHXOohuLxxc5qi1eDzsl9ucPDpg== dependencies: "@babel/code-frame" "7.5.5" - address "1.1.0" - browserslist "4.6.6" + address "1.1.2" + browserslist "4.7.0" chalk "2.4.2" cross-spawn "6.0.5" detect-port-alt "1.1.6" @@ -8499,32 +8704,41 @@ react-dev-utils@^9.0.3: loader-utils "1.2.3" open "^6.3.0" pkg-up "2.0.0" - react-error-overlay "^6.0.1" + react-error-overlay "^6.0.3" recursive-readdir "2.2.2" - shell-quote "1.6.1" - sockjs-client "1.3.0" + shell-quote "1.7.2" + sockjs-client "1.4.0" strip-ansi "5.2.0" text-table "0.2.0" -react-dom@^16.8.6: - version "16.9.0" - resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-16.9.0.tgz#5e65527a5e26f22ae3701131bcccaee9fb0d3962" - integrity sha512-YFT2rxO9hM70ewk9jq0y6sQk8cL02xm4+IzYBz75CQGlClQQ1Bxq0nhHF6OtSbit+AIahujJgb/CPRibFkMNJQ== +react-dom@^16.10.2: + version "16.10.2" + resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-16.10.2.tgz#4840bce5409176bc3a1f2bd8cb10b92db452fda6" + integrity sha512-kWGDcH3ItJK4+6Pl9DZB16BXYAZyrYQItU4OMy0jAkv5aNqc+mAKb4TpFtAteI6TJZu+9ZlNhaeNQSVQDHJzkw== dependencies: loose-envify "^1.1.0" object-assign "^4.1.1" prop-types "^15.6.2" - scheduler "^0.15.0" + scheduler "^0.16.2" -react-error-overlay@^6.0.1: - version "6.0.1" - resolved "https://registry.yarnpkg.com/react-error-overlay/-/react-error-overlay-6.0.1.tgz#b8d3cf9bb991c02883225c48044cb3ee20413e0f" - integrity sha512-V9yoTr6MeZXPPd4nV/05eCBvGH9cGzc52FN8fs0O0TVQ3HYYf1n7EgZVtHbldRq5xU9zEzoXIITjYNIfxDDdUw== +react-error-overlay@^6.0.3: + version "6.0.3" + resolved "https://registry.yarnpkg.com/react-error-overlay/-/react-error-overlay-6.0.3.tgz#c378c4b0a21e88b2e159a3e62b2f531fd63bf60d" + integrity sha512-bOUvMWFQVk5oz8Ded9Xb7WVdEi3QGLC8tH7HmYP0Fdp4Bn3qw0tRFmr5TW6mvahzvmrK4a6bqWGfCevBflP+Xw== + +react-highlight-words@^0.16.0: + version "0.16.0" + resolved "https://registry.yarnpkg.com/react-highlight-words/-/react-highlight-words-0.16.0.tgz#4b4b9824e3d2b98789d3e3b3aedb5e961ae1b7cf" + integrity sha512-q34TwCSJOL+5pVDv6LUj3amaoyXdNDwd7zRqVAvceOrO9g1haWLAglK6WkGLMNUa3PFN8EgGedLg/k8Gpndxqg== + dependencies: + highlight-words-core "^1.2.0" + memoize-one "^4.0.0" + prop-types "^15.5.8" -react-is@^16.6.0, react-is@^16.7.0, react-is@^16.8.1, react-is@^16.8.4, react-is@^16.8.6, react-is@^16.9.0: - version "16.9.0" - resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.9.0.tgz#21ca9561399aad0ff1a7701c01683e8ca981edcb" - integrity sha512-tJBzzzIgnnRfEm046qRcURvwQnZVXmuCbscxUO5RWrGTXpon2d4c8mI0D8WE6ydVIm29JiLB6+RslkIvym9Rjw== +react-is@^16.10.2, react-is@^16.6.0, react-is@^16.7.0, react-is@^16.8.1, react-is@^16.8.4, react-is@^16.8.6, react-is@^16.9.0: + version "16.10.2" + resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.10.2.tgz#984120fd4d16800e9a738208ab1fba422d23b5ab" + integrity sha512-INBT1QEgtcCCgvccr5/86CfD71fw9EPmDxgiJX4I2Ddr6ZsV6iFXsuby+qWJPtmNuMY0zByTsG4468P7nHuNWA== react-json-view@^1.19.1: version "1.19.1" @@ -8553,7 +8767,7 @@ react-popper@^1.3.3: typed-styles "^0.0.7" warning "^4.0.2" -react-redux@^7.0.1: +react-redux@^7.1.1: version "7.1.1" resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-7.1.1.tgz#ce6eee1b734a7a76e0788b3309bf78ff6b34fa0a" integrity sha512-QsW0vcmVVdNQzEkrgzh2W3Ksvr8cqpAv5FhEk7tNEft+5pp7rXxAudTz3VOPawRkLIepItpkEIyLcN/VVXzjTg== @@ -8565,23 +8779,23 @@ react-redux@^7.0.1: prop-types "^15.7.2" react-is "^16.9.0" -react-router-dom@^5.0.0: - version "5.0.1" - resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-5.0.1.tgz#ee66f4a5d18b6089c361958e443489d6bab714be" - integrity sha512-zaVHSy7NN0G91/Bz9GD4owex5+eop+KvgbxXsP/O+iW1/Ln+BrJ8QiIR5a6xNPtrdTvLkxqlDClx13QO1uB8CA== +react-router-dom@^5.1.2: + version "5.1.2" + resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-5.1.2.tgz#06701b834352f44d37fbb6311f870f84c76b9c18" + integrity sha512-7BPHAaIwWpZS074UKaw1FjVdZBSVWEk8IuDXdB+OkLb8vd/WRQIpA4ag9WQk61aEfQs47wHyjWUoUGGZxpQXew== dependencies: "@babel/runtime" "^7.1.2" history "^4.9.0" loose-envify "^1.3.1" prop-types "^15.6.2" - react-router "5.0.1" + react-router "5.1.2" tiny-invariant "^1.0.2" tiny-warning "^1.0.0" -react-router@5.0.1, react-router@^5.0.0: - version "5.0.1" - resolved "https://registry.yarnpkg.com/react-router/-/react-router-5.0.1.tgz#04ee77df1d1ab6cb8939f9f01ad5702dbadb8b0f" - integrity sha512-EM7suCPNKb1NxcTZ2LEOWFtQBQRQXecLxVpdsP4DW4PbbqYWeRiLyV/Tt1SdCrvT2jcyXAXmVTmzvSzrPR63Bg== +react-router@5.1.2, react-router@^5.1.2: + version "5.1.2" + resolved "https://registry.yarnpkg.com/react-router/-/react-router-5.1.2.tgz#6ea51d789cb36a6be1ba5f7c0d48dd9e817d3418" + integrity sha512-yjEuMFy1ONK246B+rsa0cUam5OeAQ8pyclRDgpxuSCrAlJ1qN9uZ5IgyKC7gQg0w8OM50NXHEegPh/ks9YuR2A== dependencies: "@babel/runtime" "^7.1.2" history "^4.9.0" @@ -8594,28 +8808,28 @@ react-router@5.0.1, react-router@^5.0.0: tiny-invariant "^1.0.2" tiny-warning "^1.0.0" -react-scripts@3.1.1: - version "3.1.1" - resolved "https://registry.yarnpkg.com/react-scripts/-/react-scripts-3.1.1.tgz#1796bc92447f3a2d3072c3b71ca99f88d099c48d" - integrity sha512-dbjTG9vJC61OI62hIswQYg5xHvwlxDTH6QXz6ICEuA5AqkFQWk1LKl76sk8fVL2WsyumbBc4FErALwKcEV2vNA== +react-scripts@3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/react-scripts/-/react-scripts-3.2.0.tgz#58ccd6b4ffa27f1b4d2986cbdcaa916660e9e33c" + integrity sha512-6LzuKbE2B4eFQG6i1FnTScn9HDcWBfXXnOwW9xKFPJ/E3rK8i1ufbOZ0ocKyRPxJAKdN7iqg3i7lt0+oxkSVOA== dependencies: - "@babel/core" "7.5.5" + "@babel/core" "7.6.0" "@svgr/webpack" "4.3.2" - "@typescript-eslint/eslint-plugin" "1.13.0" - "@typescript-eslint/parser" "1.13.0" - babel-eslint "10.0.2" - babel-jest "^24.8.0" + "@typescript-eslint/eslint-plugin" "^2.2.0" + "@typescript-eslint/parser" "^2.2.0" + babel-eslint "10.0.3" + babel-jest "^24.9.0" babel-loader "8.0.6" - babel-plugin-named-asset-import "^0.3.3" - babel-preset-react-app "^9.0.1" + babel-plugin-named-asset-import "^0.3.4" + babel-preset-react-app "^9.0.2" camelcase "^5.2.0" case-sensitive-paths-webpack-plugin "2.2.0" css-loader "2.1.1" dotenv "6.2.0" - dotenv-expand "4.2.0" + dotenv-expand "5.1.0" eslint "^6.1.0" - eslint-config-react-app "^5.0.1" - eslint-loader "2.2.1" + eslint-config-react-app "^5.0.2" + eslint-loader "3.0.2" eslint-plugin-flowtype "3.13.0" eslint-plugin-import "2.18.2" eslint-plugin-jsx-a11y "6.2.3" @@ -8626,11 +8840,11 @@ react-scripts@3.1.1: html-webpack-plugin "4.0.0-beta.5" identity-obj-proxy "3.0.0" is-wsl "^1.1.0" - jest "24.8.0" + jest "24.9.0" jest-environment-jsdom-fourteen "0.1.0" - jest-resolve "24.8.0" - jest-watch-typeahead "0.3.1" - mini-css-extract-plugin "0.5.0" + jest-resolve "24.9.0" + jest-watch-typeahead "0.4.0" + mini-css-extract-plugin "0.8.0" optimize-css-assets-webpack-plugin "5.0.3" pnp-webpack-plugin "1.5.0" postcss-flexbugs-fixes "4.1.0" @@ -8638,39 +8852,39 @@ react-scripts@3.1.1: postcss-normalize "7.0.1" postcss-preset-env "6.7.0" postcss-safe-parser "4.0.1" - react-app-polyfill "^1.0.2" - react-dev-utils "^9.0.3" + react-app-polyfill "^1.0.4" + react-dev-utils "^9.1.0" resolve "1.12.0" resolve-url-loader "3.1.0" sass-loader "7.2.0" semver "6.3.0" style-loader "1.0.0" terser-webpack-plugin "1.4.1" - ts-pnp "1.1.2" + ts-pnp "1.1.4" url-loader "2.1.0" - webpack "4.39.1" + webpack "4.41.0" webpack-dev-server "3.2.1" - webpack-manifest-plugin "2.0.4" + webpack-manifest-plugin "2.1.1" workbox-webpack-plugin "4.3.1" optionalDependencies: fsevents "2.0.7" -react-table@^6.10.0: - version "6.10.0" - resolved "https://registry.yarnpkg.com/react-table/-/react-table-6.10.0.tgz#20444b19d8ca3c1a08e7544e5c3a93e4ba56690e" - integrity sha512-s/mQLI1+mNvlae45MfAZyZ04YIT3jUzWJqx34s0tfwpDdgJkpeK6vyzwMUkKFCpGODBxpjBOekYZzcEmk+2FiQ== +react-table@^6.10.3: + version "6.10.3" + resolved "https://registry.yarnpkg.com/react-table/-/react-table-6.10.3.tgz#d085487a5a1b18b76486b71cf1d388d87c8c7362" + integrity sha512-sVlq2/rxVaQJywGD95+qGiMr/SMHFIFnXdx619BLOWE/Os5FOGtV6pQJNAjZixbQZiOu7dmBO1kME28uxh6wmA== dependencies: classnames "^2.2.5" -react-test-renderer@^16.0.0-0, react-test-renderer@^16.6.3: - version "16.9.0" - resolved "https://registry.yarnpkg.com/react-test-renderer/-/react-test-renderer-16.9.0.tgz#7ed657a374af47af88f66f33a3ef99c9610c8ae9" - integrity sha512-R62stB73qZyhrJo7wmCW9jgl/07ai+YzvouvCXIJLBkRlRqLx4j9RqcLEAfNfU3OxTGucqR2Whmn3/Aad6L3hQ== +react-test-renderer@^16.0.0-0, react-test-renderer@^16.10.2: + version "16.10.2" + resolved "https://registry.yarnpkg.com/react-test-renderer/-/react-test-renderer-16.10.2.tgz#4d8492f8678c9b43b721a7d79ed0840fdae7c518" + integrity sha512-k9Qzyev6cTIcIfrhgrFlYQAFxh5EEDO6ALNqYqmKsWVA7Q/rUMTay5nD3nthi6COmYsd4ghVYyi8U86aoeMqYQ== dependencies: object-assign "^4.1.1" prop-types "^15.6.2" - react-is "^16.9.0" - scheduler "^0.15.0" + react-is "^16.8.6" + scheduler "^0.16.2" react-textarea-autosize@^6.1.0: version "6.1.0" @@ -8689,25 +8903,22 @@ react-transition-group@^2.3.1: prop-types "^15.6.2" react-lifecycles-compat "^3.0.4" -react@^16.8.6: - version "16.9.0" - resolved "https://registry.yarnpkg.com/react/-/react-16.9.0.tgz#40ba2f9af13bc1a38d75dbf2f4359a5185c4f7aa" - integrity sha512-+7LQnFBwkiw+BobzOF6N//BdoNw0ouwmSJTEm9cglOOmsg/TMiFHZLe2sEoN5M7LgJTj9oHH0gxklfnQe66S1w== +react@^16.10.2: + version "16.10.2" + resolved "https://registry.yarnpkg.com/react/-/react-16.10.2.tgz#a5ede5cdd5c536f745173c8da47bda64797a4cf0" + integrity sha512-MFVIq0DpIhrHFyqLU0S3+4dIcBhhOvBE8bJ/5kHPVOVaGdo0KuiQzpcjCPsf585WvhypqtrMILyoE2th6dT+Lw== dependencies: loose-envify "^1.1.0" object-assign "^4.1.1" prop-types "^15.6.2" -reactstrap@^8.0.1: - version "8.0.1" - resolved "https://registry.yarnpkg.com/reactstrap/-/reactstrap-8.0.1.tgz#0b663c8195f540bc1d6d5dbcbcf73cab56fe7c79" - integrity sha512-GvUWEL+a2+3npK1OxTXcNBMHXX4x6uc1KQRzK7yAOl+8sAHTRWqjunvMUfny3oDh8yKVzgqpqQlWWvs1B2HR9A== +reactstrap@^8.1.1: + version "8.1.1" + resolved "https://registry.yarnpkg.com/reactstrap/-/reactstrap-8.1.1.tgz#3d911aaf773bfa66ac36355cb521f7560599f9de" + integrity sha512-m4IIdTHBT5wtcPts4w4rYngKuqIUC1BpncVxCTeIj4BQDxwjdab0gGyKJKfgXgArn6iI6syiDyez/h2tLWFjsw== dependencies: "@babel/runtime" "^7.2.0" classnames "^2.2.3" - lodash.isfunction "^3.0.9" - lodash.isobject "^3.0.2" - lodash.tonumber "^4.0.3" prop-types "^15.5.8" react-lifecycles-compat "^3.0.4" react-popper "^1.3.3" @@ -8778,6 +8989,13 @@ readdirp@^2.2.1: micromatch "^3.1.10" readable-stream "^2.0.2" +readdirp@~3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.2.0.tgz#c30c33352b12c96dfb4b895421a49fd5a9593839" + integrity sha512-crk4Qu3pmXwgxdSgGhgA/eXiJAPQiX4GMOZZMXnqKxHX7TaoL+3gQVo/WeuAiogr07DpnfjIMpXXa+PAIvwPGQ== + dependencies: + picomatch "^2.0.4" + realpath-native@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/realpath-native/-/realpath-native-1.1.0.tgz#2003294fea23fb0672f2476ebe22fcf498a2d65c" @@ -8880,11 +9098,6 @@ regex-parser@2.2.10: resolved "https://registry.yarnpkg.com/regex-parser/-/regex-parser-2.2.10.tgz#9e66a8f73d89a107616e63b39d4deddfee912b37" integrity sha512-8t6074A68gHfU8Neftl0Le6KTDwfGAj7IyjPIMSfikI2wJUTHDMaIq42bUsfVnj8mhx0R+45rdUXHGpN164avA== -regexp-tree@^0.1.6: - version "0.1.12" - resolved "https://registry.yarnpkg.com/regexp-tree/-/regexp-tree-0.1.12.tgz#28eaaa6e66eeb3527c15108a3ff740d9e574e420" - integrity sha512-TsXZ8+cv2uxMEkLfgwO0E068gsNMLfuYwMMhiUxf0Kw2Vcgzq93vgl6wIlIYuPmfMqMjfQ9zAporiozqCnwLuQ== - regexp.prototype.flags@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/regexp.prototype.flags/-/regexp.prototype.flags-1.2.0.tgz#6b30724e306a27833eeb171b66ac8890ba37e41c" @@ -8897,10 +9110,10 @@ regexpp@^2.0.1: resolved "https://registry.yarnpkg.com/regexpp/-/regexpp-2.0.1.tgz#8d19d31cf632482b589049f8281f93dbcba4d07f" integrity sha512-lv0M6+TkDVniA3aD1Eg0DVpfU/booSu7Eev3TDO/mZKHBfVjgCGTV4t4buppESEYDtkArYFOxTJWv6S5C+iaNw== -regexpu-core@^4.5.4: - version "4.5.5" - resolved "https://registry.yarnpkg.com/regexpu-core/-/regexpu-core-4.5.5.tgz#aaffe61c2af58269b3e516b61a73790376326411" - integrity sha512-FpI67+ky9J+cDizQUJlIlNZFKual/lUkFr1AG6zOCpwZ9cLrg8UUVakyUQJD7fCDIe9Z2nwTQJNPyonatNmDFQ== +regexpu-core@^4.6.0: + version "4.6.0" + resolved "https://registry.yarnpkg.com/regexpu-core/-/regexpu-core-4.6.0.tgz#2037c18b327cfce8a6fea2a4ec441f2432afb8b6" + integrity sha512-YlVaefl8P5BnFYOITTNzDvan1ulLOiXJzCNZxduTIosN17b87h3bvG9yHMoHaRuo88H4mQ06Aodj5VtYGGGiTg== dependencies: regenerate "^1.4.0" regenerate-unicode-properties "^8.1.0" @@ -8910,9 +9123,9 @@ regexpu-core@^4.5.4: unicode-match-property-value-ecmascript "^1.1.0" regjsgen@^0.5.0: - version "0.5.0" - resolved "https://registry.yarnpkg.com/regjsgen/-/regjsgen-0.5.0.tgz#a7634dc08f89209c2049adda3525711fb97265dd" - integrity sha512-RnIrLhrXCX5ow/E5/Mh2O4e/oa1/jW0eaBKTSy3LaCj+M3Bqvm97GWDp2yUtzIs4LEn65zR2yiYGFqb2ApnzDA== + version "0.5.1" + resolved "https://registry.yarnpkg.com/regjsgen/-/regjsgen-0.5.1.tgz#48f0bf1a5ea205196929c0d9798b42d1ed98443c" + integrity sha512-5qxzGZjDs9w4tzT3TPhCJqWdCc3RLYwy9J2NB0nm5Lz+S273lvWcpjaTGHsT1dc6Hhfq41uSEOw8wBmxrKOuyg== regjsparser@^0.6.0: version "0.6.0" @@ -9031,10 +9244,10 @@ resolve-from@^4.0.0: resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6" integrity sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g== -resolve-pathname@^2.2.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/resolve-pathname/-/resolve-pathname-2.2.0.tgz#7e9ae21ed815fd63ab189adeee64dc831eefa879" - integrity sha512-bAFz9ld18RzJfddgrO2e/0S2O81710++chRMUxHjXOYKF6jTAMrUNZrEZ1PvV0zlhfjidm08iRPdTLPno1FuRg== +resolve-pathname@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/resolve-pathname/-/resolve-pathname-3.0.0.tgz#99d02224d3cf263689becbb393bc560313025dcd" + integrity sha512-C7rARubxI8bXFNB/hqcp/4iUeIXJhJZvFPFPiSPRnhU5UPxzMFIl+2E6yY6c4k9giDJAhtV+enfA+G89N6Csng== resolve-url-loader@3.1.0: version "3.1.0" @@ -9062,7 +9275,7 @@ resolve@1.1.7: resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.1.7.tgz#203114d82ad2c5ed9e8e0411b3932875e889e97b" integrity sha1-IDEU2CrSxe2ejgQRs5ModeiJ6Xs= -resolve@1.12.0, resolve@^1.10.0, resolve@^1.10.1, resolve@^1.11.0, resolve@^1.3.2, resolve@^1.5.0, resolve@^1.8.1: +resolve@1.12.0, resolve@^1.10.0, resolve@^1.10.1, resolve@^1.11.0, resolve@^1.12.0, resolve@^1.3.2, resolve@^1.5.0, resolve@^1.8.1: version "1.12.0" resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.12.0.tgz#3fc644a35c84a48554609ff26ec52b66fa577df6" integrity sha512-B/dOmuoAik5bKcD6s6nXDCjzUKnaDvdkRyAk6rsmsKLipWj4797iothd7jmmUhWTfinVMU+wc56rYKsit2Qy4w== @@ -9155,9 +9368,9 @@ run-queue@^1.0.0, run-queue@^1.0.3: aproba "^1.1.1" rxjs@^6.4.0: - version "6.5.2" - resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-6.5.2.tgz#2e35ce815cd46d84d02a209fb4e5921e051dbec7" - integrity sha512-HUb7j3kvb7p7eCUHE3FqjoDsC1xfZQ4AHFWfTKSpZ+sAhhz5X1WX0ZuUqWbzB2QhSLp3DoLUG+hMdEDKqWo2Zg== + version "6.5.3" + resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-6.5.3.tgz#510e26317f4db91a7eb1de77d9dd9ba0a4899a3a" + integrity sha512-wuYsAYYFdWTAnAaPoKGNhfpWwKZbJW+HgAJ+mImp+Epl7BG8oNWBCTyRM8gba9k4lk8BgWdoYm21Mo/RYhhbgA== dependencies: tslib "^1.9.0" @@ -9221,10 +9434,10 @@ saxes@^3.1.9: dependencies: xmlchars "^2.1.1" -scheduler@^0.15.0: - version "0.15.0" - resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.15.0.tgz#6bfcf80ff850b280fed4aeecc6513bc0b4f17f8e" - integrity sha512-xAefmSfN6jqAa7Kuq7LIJY0bwAPG3xlCj0HMEBQk1lxYiDKZscY2xJ5U/61ZTrYbmNQbXa+gc7czPkVo11tnCg== +scheduler@^0.16.2: + version "0.16.2" + resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.16.2.tgz#f74cd9d33eff6fc554edfb79864868e4819132c1" + integrity sha512-BqYVWqwz6s1wZMhjFvLfVR5WXP7ZY32M/wYPo04CcuPM7XZEbV2TBNW7Z0UkguPTl0dWMA59VbNXxK6q+pHItg== dependencies: loose-envify "^1.1.0" object-assign "^4.1.1" @@ -9238,13 +9451,13 @@ schema-utils@^1.0.0: ajv-errors "^1.0.0" ajv-keywords "^3.1.0" -schema-utils@^2.0.0, schema-utils@^2.0.1: - version "2.1.0" - resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-2.1.0.tgz#940363b6b1ec407800a22951bdcc23363c039393" - integrity sha512-g6SViEZAfGNrToD82ZPUjq52KUPDYc+fN5+g6Euo5mLokl/9Yx14z0Cu4RR1m55HtBXejO0sBt+qw79axN+Fiw== +schema-utils@^2.0.0, schema-utils@^2.0.1, schema-utils@^2.2.0: + version "2.5.0" + resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-2.5.0.tgz#8f254f618d402cc80257486213c8970edfd7c22f" + integrity sha512-32ISrwW2scPXHUSusP8qMg5dLUawKkyV+/qIEV9JdXKx+rsM6mi8vZY8khg2M69Qom16rtroWXD3Ybtiws38gQ== dependencies: - ajv "^6.1.0" - ajv-keywords "^3.1.0" + ajv "^6.10.2" + ajv-keywords "^3.4.1" select-hose@^2.0.0: version "2.0.0" @@ -9252,22 +9465,17 @@ select-hose@^2.0.0: integrity sha1-Yl2GWPhlr0Psliv8N2o3NZpJlMo= selfsigned@^1.9.1: - version "1.10.4" - resolved "https://registry.yarnpkg.com/selfsigned/-/selfsigned-1.10.4.tgz#cdd7eccfca4ed7635d47a08bf2d5d3074092e2cd" - integrity sha512-9AukTiDmHXGXWtWjembZ5NDmVvP2695EtpgbCsxCa68w3c88B+alqbmZ4O3hZ4VWGXeGWzEVdvqgAJD8DQPCDw== + version "1.10.7" + resolved "https://registry.yarnpkg.com/selfsigned/-/selfsigned-1.10.7.tgz#da5819fd049d5574f28e88a9bcc6dbc6e6f3906b" + integrity sha512-8M3wBCzeWIJnQfl43IKwOmC4H/RAp50S8DF60znzjW5GVqTcSe2vWclt7hmYVPkKPlHWOu5EaWOMZ2Y6W8ZXTA== dependencies: - node-forge "0.7.5" + node-forge "0.9.0" "semver@2 || 3 || 4 || 5", semver@^5.3.0, semver@^5.4.1, semver@^5.5.0, semver@^5.5.1, semver@^5.6.0, semver@^5.7.0: version "5.7.1" resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7" integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ== -semver@5.5.0: - version "5.5.0" - resolved "https://registry.yarnpkg.com/semver/-/semver-5.5.0.tgz#dc4bbc7a6ca9d916dee5d43516f0092b58f7b8ab" - integrity sha512-4SJ3dm0WAwWy/NVeioZh5AntkdJoWKxHxcmyP622fOkgHa4z3R0TdBJICINyaSDE6uNwVc8gZr+ZinwZAH4xIA== - semver@6.3.0, semver@^6.0.0, semver@^6.1.2, semver@^6.2.0, semver@^6.3.0: version "6.3.0" resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d" @@ -9293,9 +9501,9 @@ send@0.17.1: statuses "~1.5.0" serialize-javascript@^1.7.0: - version "1.8.0" - resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-1.8.0.tgz#9515fc687232e2321aea1ca7a529476eb34bb480" - integrity sha512-3tHgtF4OzDmeKYj6V9nSyceRS0UJ3C7VqyD2Yj28vC/z2j6jG5FmFGahOKMD9CrglxTm3tETr87jEypaYV8DUg== + version "1.9.1" + resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-1.9.1.tgz#cfc200aef77b600c47da9bb8149c943e798c2fdb" + integrity sha512-0Vb/54WJ6k5v8sSWN09S0ora+Hnr+cX40r9F170nT+mSkaxltoE/7R3OrIdBSUv1OoiobH1QoWQbCnAO+e8J1A== serve-index@^1.7.2: version "1.9.1" @@ -9387,15 +9595,10 @@ shebang-regex@^1.0.0: resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-1.0.0.tgz#da42f49740c0b42db2ca9728571cb190c98efea3" integrity sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM= -shell-quote@1.6.1: - version "1.6.1" - resolved "https://registry.yarnpkg.com/shell-quote/-/shell-quote-1.6.1.tgz#f4781949cce402697127430ea3b3c5476f481767" - integrity sha1-9HgZSczkAmlxJ0MOo7PFR29IF2c= - dependencies: - array-filter "~0.0.0" - array-map "~0.0.0" - array-reduce "~0.0.0" - jsonify "~0.0.0" +shell-quote@1.7.2: + version "1.7.2" + resolved "https://registry.yarnpkg.com/shell-quote/-/shell-quote-1.7.2.tgz#67a7d02c76c9da24f99d20808fcaded0e0e04be2" + integrity sha512-mRz/m/JVscCrkMyPqHc/bczi3OQHkLTqXHEFu0zDhK/qfv3UcOA4SVmRCLmos4bhjr9ekVQubj/R7waKapmiQg== shellwords@^0.1.1: version "0.1.1" @@ -9429,6 +9632,11 @@ slash@^2.0.0: resolved "https://registry.yarnpkg.com/slash/-/slash-2.0.0.tgz#de552851a1759df3a8f206535442f5ec4ddeab44" integrity sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A== +slash@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634" + integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q== + slice-ansi@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-2.1.0.tgz#cacd7693461a637a5788d92a7dd4fba068e81636" @@ -9480,6 +9688,18 @@ sockjs-client@1.3.0: json3 "^3.3.2" url-parse "^1.4.3" +sockjs-client@1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/sockjs-client/-/sockjs-client-1.4.0.tgz#c9f2568e19c8fd8173b4997ea3420e0bb306c7d5" + integrity sha512-5zaLyO8/nri5cua0VtOrFXBPK1jbL4+1cebT/mmKA1E1ZXOvJrII75bPu0l0k843G/+iAbhEqzyKr0w/eCCj7g== + dependencies: + debug "^3.2.5" + eventsource "^1.0.7" + faye-websocket "~0.11.1" + inherits "^2.0.3" + json3 "^3.3.2" + url-parse "^1.4.3" + sockjs@0.3.19: version "0.3.19" resolved "https://registry.yarnpkg.com/sockjs/-/sockjs-0.3.19.tgz#d976bbe800af7bd20ae08598d582393508993c0d" @@ -9488,6 +9708,13 @@ sockjs@0.3.19: faye-websocket "^0.10.0" uuid "^3.0.1" +sort-keys@^1.0.0: + version "1.1.2" + resolved "https://registry.yarnpkg.com/sort-keys/-/sort-keys-1.1.2.tgz#441b6d4d346798f1b4e49e8920adfba0e543f9ad" + integrity sha1-RBttTTRnmPG05J6JIK37oOVD+a0= + dependencies: + is-plain-obj "^1.0.0" + source-list-map@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/source-list-map/-/source-list-map-2.0.1.tgz#3993bd873bfc48479cca9ea3a547835c7c154b34" @@ -9670,6 +9897,11 @@ stream-shift@^1.0.0: resolved "https://registry.yarnpkg.com/stream-shift/-/stream-shift-1.0.0.tgz#d5c752825e5367e786f78e18e445ea223a155952" integrity sha1-1cdSgl5TZ+eG944Y5EXqIjoVWVI= +strict-uri-encode@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/strict-uri-encode/-/strict-uri-encode-1.1.0.tgz#279b225df1d582b1f54e65addd4352e18faa0713" + integrity sha1-J5siXfHVgrH1TmWt3UNS4Y+qBxM= + string-length@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/string-length/-/string-length-2.0.0.tgz#d40dbb686a3ace960c1cffca562bf2c45f8363ed" @@ -9678,6 +9910,14 @@ string-length@^2.0.0: astral-regex "^1.0.0" strip-ansi "^4.0.0" +string-length@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/string-length/-/string-length-3.1.0.tgz#107ef8c23456e187a8abd4a61162ff4ac6e25837" + integrity sha512-Ttp5YvkGm5v9Ijagtaz1BnN+k9ObpvS0eIBblPMp2YWL8FBmi9qblQ9fexc2k/CXFgrTIteU3jAw3payCnwSTA== + dependencies: + astral-regex "^1.0.0" + strip-ansi "^5.2.0" + string-width@^1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/string-width/-/string-width-1.0.2.tgz#118bdf5b8cdc51a2a7e70d211e07e2b0b9b107d3" @@ -9713,6 +9953,22 @@ string.prototype.trim@^1.1.2: es-abstract "^1.13.0" function-bind "^1.1.1" +string.prototype.trimleft@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/string.prototype.trimleft/-/string.prototype.trimleft-2.1.0.tgz#6cc47f0d7eb8d62b0f3701611715a3954591d634" + integrity sha512-FJ6b7EgdKxxbDxc79cOlok6Afd++TTs5szo+zJTUyow3ycrRfJVE2pq3vcN53XexvKZu/DJMDfeI/qMiZTrjTw== + dependencies: + define-properties "^1.1.3" + function-bind "^1.1.1" + +string.prototype.trimright@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/string.prototype.trimright/-/string.prototype.trimright-2.1.0.tgz#669d164be9df9b6f7559fa8e89945b168a5a6c58" + integrity sha512-fXZTSV55dNBwv16uw+hh5jkghxSnc5oHq+5K/gXgizHwAvMetdAJlHqqoFC1FSDVPYWLkAKl2cxpUT41sV7nSg== + dependencies: + define-properties "^1.1.3" + function-bind "^1.1.1" + string_decoder@^1.0.0, string_decoder@^1.1.1: version "1.3.0" resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e" @@ -9887,13 +10143,13 @@ tapable@^1.0.0, tapable@^1.1.0, tapable@^1.1.3: integrity sha512-4WK/bYZmj8xLr+HUCODHGF1ZFzsYffasLUgEiMBY4fgtltdO6B4WJtlSbPaDTLpYTcGVwM2qLnFTICEcNxs3kA== tar@^4: - version "4.4.10" - resolved "https://registry.yarnpkg.com/tar/-/tar-4.4.10.tgz#946b2810b9a5e0b26140cf78bea6b0b0d689eba1" - integrity sha512-g2SVs5QIxvo6OLp0GudTqEf05maawKUxXru104iaayWA09551tFCTI8f1Asb4lPfkBr91k07iL4c11XO3/b0tA== + version "4.4.13" + resolved "https://registry.yarnpkg.com/tar/-/tar-4.4.13.tgz#43b364bc52888d555298637b10d60790254ab525" + integrity sha512-w2VwSrBoHa5BsSyH+KxEqeQBAllHhccyMFVHtGtdMpF4W7IRWfZjFiQceJPChOeTsSDVUpER2T8FA93pr0L+QA== dependencies: chownr "^1.1.1" fs-minipass "^1.2.5" - minipass "^2.3.5" + minipass "^2.8.6" minizlib "^1.2.1" mkdirp "^0.5.0" safe-buffer "^5.1.2" @@ -9915,9 +10171,9 @@ terser-webpack-plugin@1.4.1, terser-webpack-plugin@^1.4.1: worker-farm "^1.7.0" terser@^4.1.2: - version "4.2.1" - resolved "https://registry.yarnpkg.com/terser/-/terser-4.2.1.tgz#1052cfe17576c66e7bc70fcc7119f22b155bdac1" - integrity sha512-cGbc5utAcX4a9+2GGVX4DsenG6v0x3glnDi5hx8816X1McEAwPlPgRtXPJzSBsbpILxZ8MQMT0KvArLuE0HP5A== + version "4.3.9" + resolved "https://registry.yarnpkg.com/terser/-/terser-4.3.9.tgz#e4be37f80553d02645668727777687dad26bbca8" + integrity sha512-NFGMpHjlzmyOtPL+fDw3G7+6Ueh/sz4mkaUYa4lJCxOPTNzd0Uj0aZJOmsDYoSQyfuVoWDMSWTPU3huyOm2zdA== dependencies: commander "^2.20.0" source-map "~0.6.1" @@ -9957,9 +10213,9 @@ through@^2.3.6: integrity sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU= thunky@^1.0.2: - version "1.0.3" - resolved "https://registry.yarnpkg.com/thunky/-/thunky-1.0.3.tgz#f5df732453407b09191dae73e2a8cc73f381a826" - integrity sha512-YwT8pjmNcAXBZqrubu22P4FYsh2D4dxRmnWBOL8Jk8bUcRUtc5326kx32tuTmFDAZtLOGEVNl8POAR8j896Iow== + version "1.1.0" + resolved "https://registry.yarnpkg.com/thunky/-/thunky-1.1.0.tgz#5abaf714a9405db0504732bbccd2cedd9ef9537d" + integrity sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA== timers-browserify@^2.0.4: version "2.0.11" @@ -10032,6 +10288,13 @@ to-regex-range@^2.1.0: is-number "^3.0.0" repeat-string "^1.6.1" +to-regex-range@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4" + integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ== + dependencies: + is-number "^7.0.0" + to-regex@^3.0.1, to-regex@^3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/to-regex/-/to-regex-3.0.2.tgz#13cfdd9b336552f30b51f33a8ae1b42a7a7599ce" @@ -10077,22 +10340,17 @@ tr46@^1.0.1: dependencies: punycode "^2.1.0" -trim-right@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/trim-right/-/trim-right-1.0.1.tgz#cb2e1203067e0c8de1f614094b9fe45704ea6003" - integrity sha1-yy4SAwZ+DI3h9hQJS5/kVwTqYAM= - -ts-pnp@1.1.2, ts-pnp@^1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/ts-pnp/-/ts-pnp-1.1.2.tgz#be8e4bfce5d00f0f58e0666a82260c34a57af552" - integrity sha512-f5Knjh7XCyRIzoC/z1Su1yLLRrPrFCgtUAh/9fCSP6NKbATwpOL1+idQVXQokK9GRFURn/jYPGPfegIctwunoA== +ts-pnp@1.1.4, ts-pnp@^1.1.2: + version "1.1.4" + resolved "https://registry.yarnpkg.com/ts-pnp/-/ts-pnp-1.1.4.tgz#ae27126960ebaefb874c6d7fa4729729ab200d90" + integrity sha512-1J/vefLC+BWSo+qe8OnJQfWTYRS6ingxjwqmHMqaMxXMj7kFtKLgAaYW3JeX3mktjgUL+etlU8/B4VUAUI9QGw== tslib@^1.8.1, tslib@^1.9.0: version "1.10.0" resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.10.0.tgz#c3c19f95973fb0a62973fb09d90d961ee43e5c8a" integrity sha512-qOebF53frne81cf0S9B41ByenJ3/IuH8yJKngAX35CmiZySA0khhkovshKK+jGCaMnVomla7gVlIcc3EvKPbTQ== -tsutils@^3.7.0: +tsutils@^3.17.1: version "3.17.1" resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-3.17.1.tgz#ed719917f11ca0dee586272b2ac49e015a2dd759" integrity sha512-kzeQ5B8H3w60nFY2g8cJIuH7JDpsALXySGtwGJ0p2LSjLgay3NdIpqq5SoOBe46bKDW2iq25irHCr8wjomUS2g== @@ -10123,6 +10381,11 @@ type-check@~0.3.2: dependencies: prelude-ls "~1.1.2" +type-fest@^0.5.2: + version "0.5.2" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.5.2.tgz#d6ef42a0356c6cd45f49485c3b6281fc148e48a2" + integrity sha512-DWkS49EQKVX//Tbupb9TFa19c7+MK1XmzkrZUR8TAktmE/DizXoaoJV6TZ/tSIPXipqNiRI6CyAe7x69Jb6RSw== + type-is@~1.6.17, type-is@~1.6.18: version "1.6.18" resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131" @@ -10132,9 +10395,9 @@ type-is@~1.6.17, type-is@~1.6.18: mime-types "~2.1.24" type@^1.0.1: - version "1.0.3" - resolved "https://registry.yarnpkg.com/type/-/type-1.0.3.tgz#16f5d39f27a2d28d86e48f8981859e9d3296c179" - integrity sha512-51IMtNfVcee8+9GJvj0spSuFcZHe9vSib6Xtgsny1Km9ugyz2mbS08I3rsUIRYgJohFRFU1160sgRodYz378Hg== + version "1.2.0" + resolved "https://registry.yarnpkg.com/type/-/type-1.2.0.tgz#848dd7698dafa3e54a6c479e759c4bc3f18847a0" + integrity sha512-+5nt5AAniqsCnu2cEQQdpzCAh33kVx8n0VoFidKpB1dVVLAN/F+bgVOqOJqOnEnrhp222clB5p3vUlD+1QAnfg== typed-styles@^0.0.7: version "0.0.7" @@ -10160,11 +10423,11 @@ uglify-js@3.4.x: source-map "~0.6.1" uglify-js@^3.1.4: - version "3.6.0" - resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.6.0.tgz#704681345c53a8b2079fb6cec294b05ead242ff5" - integrity sha512-W+jrUHJr3DXKhrsS7NUVxn3zqMOFn0hL/Ei6v0anCIMoKC93TjcflTagwIHLW7SfMFfiQuktQyFVCFHGUE0+yg== + version "3.6.3" + resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.6.3.tgz#1351533bbe22cc698f012589ed6bd4cbd971bff8" + integrity sha512-KfQUgOqTkLp2aZxrMbCuKCDGW9slFYu2A23A36Gs7sGzTLcRBDORdOi5E21KWHFIfkY8kzgi/Pr1cXCh0yIp5g== dependencies: - commander "~2.20.0" + commander "~2.20.3" source-map "~0.6.1" unfetch@^4.1.0: @@ -10253,9 +10516,9 @@ unset-value@^1.0.0: isobject "^3.0.0" upath@^1.1.1: - version "1.1.2" - resolved "https://registry.yarnpkg.com/upath/-/upath-1.1.2.tgz#3db658600edaeeccbe6db5e684d67ee8c2acd068" - integrity sha512-kXpym8nmDmlCBr7nKdIx8P2jNBa+pBpIUFRnKJ4dr8htyYGJFokkr2ZvERRtUN+9SY+JqXouNgUPtv6JQva/2Q== + version "1.2.0" + resolved "https://registry.yarnpkg.com/upath/-/upath-1.2.0.tgz#8f66dbcd55a883acdae4408af8b035a5044c1894" + integrity sha512-aZwGpamFO61g3OlfT7OQCHqhGnW43ieH9WZeP7QxN/G/jS4jfqUkZxoryvJgVPEcrl5NL/ggHsSmLMHuH64Lhg== upper-case@^1.1.1: version "1.1.3" @@ -10369,10 +10632,10 @@ validate-npm-package-license@^3.0.1: spdx-correct "^3.0.0" spdx-expression-parse "^3.0.0" -value-equal@^0.4.0: - version "0.4.0" - resolved "https://registry.yarnpkg.com/value-equal/-/value-equal-0.4.0.tgz#c5bdd2f54ee093c04839d71ce2e4758a6890abc7" - integrity sha512-x+cYdNnaA3CxvMaTX0INdTCN8m8aF2uY9BvEqmxuYp8bL09cs/kWVQPVGcA35fMktdOsP69IgU7wFj/61dJHEw== +value-equal@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/value-equal/-/value-equal-1.0.1.tgz#1e0b794c734c5c0cade179c437d356d931a34d6c" + integrity sha512-NOJ6JZCAWr0zlxZt+xqCHNTEKOsrks2HQd4MqhP1qy4z1SkbEP467eNx6TgDKXMvUOb+OENfJCZwM+16n7fRfw== vary@~1.1.2: version "1.1.2" @@ -10450,12 +10713,13 @@ webidl-conversions@^4.0.2: integrity sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg== webpack-dev-middleware@^3.5.1: - version "3.7.0" - resolved "https://registry.yarnpkg.com/webpack-dev-middleware/-/webpack-dev-middleware-3.7.0.tgz#ef751d25f4e9a5c8a35da600c5fda3582b5c6cff" - integrity sha512-qvDesR1QZRIAZHOE3iQ4CXLZZSQ1lAUsSpnQmlB1PBfoN/xdRjmge3Dok0W4IdaVLJOGJy3sGI4sZHwjRU0PCA== + version "3.7.2" + resolved "https://registry.yarnpkg.com/webpack-dev-middleware/-/webpack-dev-middleware-3.7.2.tgz#0019c3db716e3fa5cecbf64f2ab88a74bab331f3" + integrity sha512-1xC42LxbYoqLNAhV6YzTYacicgMZQTqRd27Sim9wn5hJrX3I5nxYy1SxSd4+gjUFsz1dQFj+yEe6zEVmSkeJjw== dependencies: memory-fs "^0.4.1" - mime "^2.4.2" + mime "^2.4.4" + mkdirp "^0.5.1" range-parser "^1.2.1" webpack-log "^2.0.0" @@ -10503,13 +10767,14 @@ webpack-log@^2.0.0: ansi-colors "^3.0.0" uuid "^3.3.2" -webpack-manifest-plugin@2.0.4: - version "2.0.4" - resolved "https://registry.yarnpkg.com/webpack-manifest-plugin/-/webpack-manifest-plugin-2.0.4.tgz#e4ca2999b09557716b8ba4475fb79fab5986f0cd" - integrity sha512-nejhOHexXDBKQOj/5v5IZSfCeTO3x1Dt1RZEcGfBSul891X/eLIcIVH31gwxPDdsi2Z8LKKFGpM4w9+oTBOSCg== +webpack-manifest-plugin@2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/webpack-manifest-plugin/-/webpack-manifest-plugin-2.1.1.tgz#6b3e280327815b83152c79f42d0ca13b665773c4" + integrity sha512-2zqJ6mvc3yoiqfDjghAIpljhLSDh/G7vqGrzYcYqqRCd/ZZZCAuc/YPE5xG0LGpLgDJRhUNV1H+znyyhIxahzA== dependencies: fs-extra "^7.0.0" lodash ">=3.5 <5" + object.entries "^1.1.0" tapable "^1.0.0" webpack-sources@^1.1.0, webpack-sources@^1.4.0, webpack-sources@^1.4.1: @@ -10520,10 +10785,10 @@ webpack-sources@^1.1.0, webpack-sources@^1.4.0, webpack-sources@^1.4.1: source-list-map "^2.0.0" source-map "~0.6.1" -webpack@4.39.1: - version "4.39.1" - resolved "https://registry.yarnpkg.com/webpack/-/webpack-4.39.1.tgz#60ed9fb2b72cd60f26ea526c404d2a4cc97a1bd8" - integrity sha512-/LAb2TJ2z+eVwisldp3dqTEoNhzp/TLCZlmZm3GGGAlnfIWDgOEE758j/9atklNLfRyhKbZTCOIoPqLJXeBLbQ== +webpack@4.41.0: + version "4.41.0" + resolved "https://registry.yarnpkg.com/webpack/-/webpack-4.41.0.tgz#db6a254bde671769f7c14e90a1a55e73602fc70b" + integrity sha512-yNV98U4r7wX1VJAj5kyMsu36T8RPPQntcb5fJLOsMz/pt/WrKC0Vp1bAlqPLkA1LegSwQwf6P+kAbyhRKVQ72g== dependencies: "@webassemblyjs/ast" "1.8.5" "@webassemblyjs/helper-module-context" "1.8.5" @@ -10839,9 +11104,9 @@ xml-name-validator@^3.0.0: integrity sha512-A5CUptxDsvxKJEU3yO6DuWBSJz/qizqzJKOMIfUJHETbBw/sFaDxgd6fxm1ewUaM0jZ444Fc5vC5ROYurg/4Pw== xmlchars@^2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/xmlchars/-/xmlchars-2.1.1.tgz#ef1a81c05bff629c2280007f12daca21bd6f6c93" - integrity sha512-7hew1RPJ1iIuje/Y01bGD/mXokXxegAgVS+e+E0wSi2ILHQkYAH1+JXARwTjZSM4Z4Z+c73aKspEcqj+zPPL/w== + version "2.2.0" + resolved "https://registry.yarnpkg.com/xmlchars/-/xmlchars-2.2.0.tgz#060fe1bcb7f9c76fe2a17db86a9bc3ab894210cb" + integrity sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw== xregexp@4.0.0: version "4.0.0" @@ -10859,9 +11124,9 @@ xtend@^4.0.0, xtend@~4.0.1: integrity sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w== yallist@^3.0.0, yallist@^3.0.2, yallist@^3.0.3: - version "3.0.3" - resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.0.3.tgz#b4b049e314be545e3ce802236d6cd22cd91c3de9" - integrity sha512-S+Zk8DEWE6oKpV+vI3qWkaK+jSbIK86pCwe2IF/xwIpQ8jEuxpw9NyaGjmp9+BoJv5FV2piqCDcoCtStppiq2A== + version "3.1.1" + resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.1.1.tgz#dbb7daf9bfd8bac9ab45ebf602b8cbad0d5d08fd" + integrity sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g== yargs-parser@^10.1.0: version "10.1.0" diff --git a/imei-lookup/build.gradle.kts b/imei-lookup/build.gradle.kts index edfc6e0d8..b265a9197 100644 --- a/imei-lookup/build.gradle.kts +++ b/imei-lookup/build.gradle.kts @@ -14,4 +14,4 @@ dependencies { testImplementation(kotlin("test-junit")) } -apply(from = "../gradle/jacoco.gradle") \ No newline at end of file +apply(from = "../gradle/jacoco.gradle.kts") \ No newline at end of file diff --git a/jersey/build.gradle.kts b/jersey/build.gradle.kts index 69d8af8c3..446370f32 100644 --- a/jersey/build.gradle.kts +++ b/jersey/build.gradle.kts @@ -15,4 +15,4 @@ dependencies { testRuntimeOnly("io.jsonwebtoken:jjwt-jackson:${Version.jjwt}") } -apply(from = "../gradle/jacoco.gradle") \ No newline at end of file +apply(from = "../gradle/jacoco.gradle.kts") \ No newline at end of file diff --git a/kts-engine/build.gradle.kts b/kts-engine/build.gradle.kts index ceba37144..fd49560a8 100644 --- a/kts-engine/build.gradle.kts +++ b/kts-engine/build.gradle.kts @@ -24,4 +24,4 @@ dependencies { testImplementation("org.mockito:mockito-core:${Version.mockito}") } -apply(from = "../gradle/jacoco.gradle") \ No newline at end of file +apply(from = "../gradle/jacoco.gradle.kts") \ No newline at end of file diff --git a/logging/build.gradle.kts b/logging/build.gradle.kts index 8d7eaa4e6..963beaf8c 100644 --- a/logging/build.gradle.kts +++ b/logging/build.gradle.kts @@ -14,4 +14,4 @@ dependencies { implementation("com.fasterxml.jackson.core:jackson-databind:${Version.jacksonDatabind}") } -apply(from = "../gradle/jacoco.gradle") \ No newline at end of file +apply(from = "../gradle/jacoco.gradle.kts") \ No newline at end of file diff --git a/model/src/main/kotlin/org/ostelco/prime/model/Entities.kt b/model/src/main/kotlin/org/ostelco/prime/model/Entities.kt index 27c8e883e..a1360748a 100644 --- a/model/src/main/kotlin/org/ostelco/prime/model/Entities.kt +++ b/model/src/main/kotlin/org/ostelco/prime/model/Entities.kt @@ -62,6 +62,7 @@ data class RegionDetails( enum class CustomerRegionStatus { PENDING, // eKYC initiated, but not yet approved APPROVED, // eKYC approved + AVAILABLE // Region is available for provisioning } enum class KycType { @@ -202,7 +203,7 @@ enum class VendorScanData(val s: String) { } enum class FCMStrings(val s: String) { - NOTIFICATION_TITLE("eKYC Status"), + JUMIO_NOTIFICATION_TITLE("eKYC Status"), JUMIO_IDENTITY_VERIFIED("Successfully verified the identity"), JUMIO_IDENTITY_FAILED("Failed to verify the identity") } diff --git a/neo4j-store/build.gradle.kts b/neo4j-store/build.gradle.kts index fe474adf4..e675bd518 100644 --- a/neo4j-store/build.gradle.kts +++ b/neo4j-store/build.gradle.kts @@ -22,4 +22,4 @@ dependencies { testImplementation("org.mockito:mockito-core:${Version.mockito}") } -apply(from = "../gradle/jacoco.gradle") \ No newline at end of file +apply(from = "../gradle/jacoco.gradle.kts") \ No newline at end of file diff --git a/neo4j-store/src/main/cypher/schema.cypher b/neo4j-store/src/main/cypher/schema.cypher new file mode 100644 index 000000000..ea2418c68 --- /dev/null +++ b/neo4j-store/src/main/cypher/schema.cypher @@ -0,0 +1,109 @@ +// Start +MATCH (n) DETACH DELETE n; +MATCH (n) RETURN n; + +// Segment-Offer-Product + +CREATE (:Segment {id:"Segment"}); + +CREATE (:Offer {id:"Offer"}); + +MATCH (o:Offer), (s:Segment) +CREATE (o)-[:OFFERED_TO_SEGMENT]->(s); + +CREATE (:Product {id:"Product"}); + +MATCH (o:Offer {id:"Offer"}), (p:Product {id:"Product"}) +CREATE (o)-[:OFFER_HAS_PRODUCT]->(p); + +// Region +CREATE (:Region {id:"Region"}); + + + +// Customer is created. + +CREATE (:Identity {id:"Identity"}); + +CREATE (:Customer {id:"Customer"}); + +MATCH (i:Identity {id:"Identity"}), (c:Customer {id:"Customer"}) +CREATE (i)-[:IDENTIFIES]->(c); + +// Customer might be referred. + +MATCH (c1:Customer {id:"Customer"}), (c2:Customer {id:"Customer"}) +CREATE (c2)-[:REFERRED]->(c2); + +// Customer gets assigned a Bundle. + +CREATE (:Bundle {id:"Bundle"}); + +MATCH (c:Customer {id:"Customer"}), (b:Bundle {id:"Bundle"}) +CREATE (c)-[:HAS_BUNDLE]->(b); + + +// Customer gets a welcome pack. + +CREATE (:PurchaseRecord {id:"PurchaseRecord"}); + +MATCH (c:Customer {id:"Customer"}), (p:PurchaseRecord {id:"PurchaseRecord"}) +CREATE (c)<-[:FOR_PURCHASE_BY]-(p); + +MATCH (pr:PurchaseRecord {id:"PurchaseRecord"}), (p:Product {id:"Product"}) +CREATE (pr)-[:FOR_PURCHASE_OF]->(p); + +// Customer performs eKYC + +CREATE (:ScanInformation {id:"ScanInformation"}); + +MATCH (c:Customer {id:"Customer"}), (s:ScanInformation {id:"ScanInformation"}) +CREATE (c)-[:EKYC_SCAN]->(s); + + +// Customer is approved for a Region +MATCH (c:Customer {id:"Customer"}), (r:Region {id:"Region"}) +CREATE (c)-[:BELONG_TO_REGION]->(r); + +// Customer gets a SimProfile for a Region +CREATE (:SimProfile {id:"SimProfile"}); + +MATCH (c:Customer {id:"Customer"}), (s:SimProfile {id:"SimProfile"}) +CREATE (c)-[:HAS_SIM_PROFILE]->(s); + +MATCH (s:SimProfile {id:"SimProfile"}), (r:Region {id:"Region"}) +CREATE (s)-[:SIM_PROFILE_FOR_REGION]->(r); + +// SimProfile has a Subscription. +CREATE (:Subscription {id:"Subscription"}); + +MATCH (sn:Subscription {id:"Subscription"}), (sp:SimProfile {id:"SimProfile"}) +CREATE (sn)-[:SUBSCRIPTION_UNDER_SIM_PROFILE]->(sp); + +MATCH (c:Customer {id:"Customer"}), (s:Subscription {id:"Subscription"}) +CREATE (c)-[:HAS_SUBSCRIPTION]->(s); + + +MATCH (s:Subscription {id:"Subscription"}), (b:Bundle {id:"Bundle"}) +CREATE (s)-[:LINKED_TO_BUNDLE]->(b); + + +// Plan + +CREATE (:Plan {id:"Plan"}); + +MATCH (c:Customer {id:"Customer"}), (p:Plan {id:"Plan"}) +CREATE (c)-[:SUBSCRIBES_TO_PLAN]->(p); + +// Customer is put in a segment. +MATCH (c:Customer {id:"Customer"}), (s:Segment {id:"Segment"}) +CREATE (c)-[:BELONG_TO_SEGMENT]->(s); + + + + + + + + + diff --git a/neo4j-store/src/main/kotlin/org/ostelco/prime/dsl/Syntax.kt b/neo4j-store/src/main/kotlin/org/ostelco/prime/dsl/Syntax.kt index be46c27f9..0cf01877b 100644 --- a/neo4j-store/src/main/kotlin/org/ostelco/prime/dsl/Syntax.kt +++ b/neo4j-store/src/main/kotlin/org/ostelco/prime/dsl/Syntax.kt @@ -9,10 +9,16 @@ import org.ostelco.prime.model.PurchaseRecord import org.ostelco.prime.model.Region import org.ostelco.prime.model.ScanInformation import org.ostelco.prime.model.Subscription +import org.ostelco.prime.storage.graph.Neo4jStoreSingleton.customerRegionRelation import org.ostelco.prime.storage.graph.Neo4jStoreSingleton.customerToBundleRelation +import org.ostelco.prime.storage.graph.Neo4jStoreSingleton.customerToSegmentRelation import org.ostelco.prime.storage.graph.Neo4jStoreSingleton.customerToSimProfileRelation +import org.ostelco.prime.storage.graph.Neo4jStoreSingleton.exCustomerRegionRelation +import org.ostelco.prime.storage.graph.Neo4jStoreSingleton.exCustomerToSimProfileRelation +import org.ostelco.prime.storage.graph.Neo4jStoreSingleton.exSubscriptionRelation +import org.ostelco.prime.storage.graph.Neo4jStoreSingleton.forPurchaseByRelation +import org.ostelco.prime.storage.graph.Neo4jStoreSingleton.forPurchaseOfRelation import org.ostelco.prime.storage.graph.Neo4jStoreSingleton.identifiesRelation -import org.ostelco.prime.storage.graph.Neo4jStoreSingleton.purchaseRecordRelation import org.ostelco.prime.storage.graph.Neo4jStoreSingleton.referredRelation import org.ostelco.prime.storage.graph.Neo4jStoreSingleton.scanInformationRelation import org.ostelco.prime.storage.graph.Neo4jStoreSingleton.simProfileRegionRelation @@ -21,7 +27,9 @@ import org.ostelco.prime.storage.graph.Neo4jStoreSingleton.subscriptionRelation import org.ostelco.prime.storage.graph.Neo4jStoreSingleton.subscriptionSimProfileRelation import org.ostelco.prime.storage.graph.Neo4jStoreSingleton.subscriptionToBundleRelation import org.ostelco.prime.storage.graph.RelationType +import org.ostelco.prime.storage.graph.model.ExCustomer import org.ostelco.prime.storage.graph.model.Identity +import org.ostelco.prime.storage.graph.model.Segment import org.ostelco.prime.storage.graph.model.SimProfile import kotlin.reflect.KClass @@ -113,10 +121,28 @@ data class CustomerContext(override val id: String) : EntityContext(Cu fromId = id, toId = plan.id) - infix fun purchased(product: ProductContext) = PartialRelationExpression( - relationType = purchaseRecordRelation, + infix fun belongsToSegment(segment: SegmentContext) = RelationExpression( + relationType = customerToSegmentRelation, fromId = id, - toId = product.id) + toId = segment.id) +} + +data class ExCustomerContext(override val id: String) : EntityContext(ExCustomer::class, id) { + + infix fun had(simProfile: SimProfileContext) = RelationExpression( + relationType = exCustomerToSimProfileRelation, + fromId = id, + toId = simProfile.id) + + infix fun subscribedTo(subscription: SubscriptionContext) = RelationExpression( + relationType = exSubscriptionRelation, + fromId = id, + toId = subscription.id) + + infix fun belongedTo(region: RegionContext) = RelationExpression( + relationType = exCustomerRegionRelation, + fromId = id, + toId = region.id) } data class BundleContext(override val id: String) : EntityContext(Bundle::class, id) @@ -146,6 +172,22 @@ data class SubscriptionContext(override val id: String) : EntityContext(ScanInformation::class, id) data class PlanContext(override val id: String) : EntityContext(Plan::class, id) data class ProductContext(override val id: String) : EntityContext(Product::class, id) +data class SegmentContext(override val id: String) : EntityContext(Segment::class, id) + +data class PurchaseRecordContext(override val id: String) : EntityContext(PurchaseRecord::class, id) { + + infix fun forPurchaseBy(customer: CustomerContext) = RelationExpression( + relationType = forPurchaseByRelation, + fromId = id, + toId = customer.id + ) + + infix fun forPurchaseOf(product: ProductContext) = RelationExpression( + relationType = forPurchaseOfRelation, + fromId = id, + toId = product.id + ) +} // // Identity @@ -183,6 +225,12 @@ infix fun Customer.Companion.referredBy(customer: CustomerContext) = fromId = customer.id ) +// +// ExCustomer +// + +infix fun ExCustomer.Companion.withId(id: String): ExCustomerContext = ExCustomerContext(id) + // // Bundle // @@ -207,6 +255,18 @@ infix fun Region.Companion.linkedToSimProfile(simProfile: SimProfileContext) = fromId = simProfile.id ) +infix fun Region.Companion.linkedToCustomer(customer: CustomerContext) = + RelatedFromClause( + relationType = customerRegionRelation, + fromId = customer.id + ) + +infix fun Region.Companion.linkedToExCustomer(exCustomer: ExCustomerContext) = + RelatedFromClause( + relationType = exCustomerRegionRelation, + fromId = exCustomer.id + ) + // // SimProfiles // @@ -225,6 +285,11 @@ infix fun SimProfile.Companion.forCustomer(customer: CustomerContext) = fromId = customer.id ) +infix fun SimProfile.Companion.forExCustomer(exCustomer: ExCustomerContext) = + RelatedFromClause( + relationType = exCustomerToSimProfileRelation, + fromId = exCustomer.id + ) // // Subscription // @@ -243,6 +308,12 @@ infix fun Subscription.Companion.subscribedBy(customer: CustomerContext) = fromId = customer.id ) +infix fun Subscription.Companion.wasSubscribedBy(exCustomer: ExCustomerContext) = + RelatedFromClause( + relationType = exSubscriptionRelation, + fromId = exCustomer.id + ) + // // ScanInfo // @@ -273,17 +344,24 @@ infix fun Plan.Companion.forCustomer(customer: CustomerContext) = infix fun Product.Companion.withSku(id: String): ProductContext = ProductContext(id) -infix fun Product.Companion.purchasedBy(customer: CustomerContext) = - RelatedFromClause( - relationType = purchaseRecordRelation, - fromId = customer.id +// +// Purchase Record +// +infix fun PurchaseRecord.Companion.withId(id: String): PurchaseRecordContext = PurchaseRecordContext(id) + +infix fun PurchaseRecord.Companion.forPurchaseBy(customer: CustomerContext) = + RelatedToClause( + relationType = forPurchaseByRelation, + toId = customer.id + ) + +infix fun PurchaseRecord.Companion.forPurchaseOf(product: ProductContext) = + RelatedToClause( + relationType = forPurchaseOfRelation, + toId = product.id ) // -// Purchase Record +// Segment // -infix fun PurchaseRecord.Companion.forPurchasesBy(customer: CustomerContext) = - RelationFromClause( - relationType = purchaseRecordRelation, - fromId = customer.id - ) \ No newline at end of file +infix fun Segment.Companion.withId(id: String): SegmentContext = SegmentContext(id) diff --git a/neo4j-store/src/main/kotlin/org/ostelco/prime/dsl/Transactions.kt b/neo4j-store/src/main/kotlin/org/ostelco/prime/dsl/Transactions.kt index cea51cb1d..8953707bf 100644 --- a/neo4j-store/src/main/kotlin/org/ostelco/prime/dsl/Transactions.kt +++ b/neo4j-store/src/main/kotlin/org/ostelco/prime/dsl/Transactions.kt @@ -11,7 +11,6 @@ import org.ostelco.prime.model.HasId import org.ostelco.prime.storage.DatabaseError import org.ostelco.prime.storage.StoreError import org.ostelco.prime.storage.SystemError -import org.ostelco.prime.storage.graph.ChangeableRelationStore import org.ostelco.prime.storage.graph.EntityRegistry import org.ostelco.prime.storage.graph.EntityStore import org.ostelco.prime.storage.graph.Neo4jClient @@ -158,10 +157,6 @@ class WriteTransaction(override val transaction: PrimeTransaction) : ReadTransac ) } } - is ChangeableRelationStore<*, *, *> -> { - logger.error("Using create on ChangeableRelationStore for relation - {}", relationExpression.relationType.name) - SystemError(type = "relationStore", id = relationExpression.relationType.name, message = "Invalid relation store").left() - } null -> { SystemError(type = "relationStore", id = relationExpression.relationType.name, message = "Missing relation store").left() } @@ -218,11 +213,6 @@ class JobContext(private val transaction: PrimeTransaction) { transaction = transaction) } } - is ChangeableRelationStore<*, *, *> -> { - result = result.flatMap { - SystemError(type = relationType.name, id = "", message = "Unable to create changable relation").left() - } - } } } } diff --git a/neo4j-store/src/main/kotlin/org/ostelco/prime/storage/graph/KtScripts.kt b/neo4j-store/src/main/kotlin/org/ostelco/prime/storage/graph/KtScripts.kt index 763047839..bacfcd80d 100644 --- a/neo4j-store/src/main/kotlin/org/ostelco/prime/storage/graph/KtScripts.kt +++ b/neo4j-store/src/main/kotlin/org/ostelco/prime/storage/graph/KtScripts.kt @@ -1,15 +1,33 @@ package org.ostelco.prime.storage.graph import arrow.core.Either +import org.ostelco.prime.model.Customer import org.ostelco.prime.model.Identity import org.ostelco.prime.storage.StoreError +interface OnNewCustomerAction { + fun apply( + identity: Identity, + customer: Customer, + transaction: PrimeTransaction + ): Either +} + +interface AllowedRegionsService { + fun get(identity: Identity, + customer: Customer, + transaction: PrimeTransaction + ): Either> +} + +interface OnRegionApprovedAction { + fun apply( + customer: Customer, + regionCode: String, + transaction: PrimeTransaction + ): Either +} + interface HssNameLookupService { fun getHssName(regionCode: String): String } - -interface OnNewCustomerAction { - fun apply(identity: Identity, - customerId: String, - transaction: PrimeTransaction): Either -} \ No newline at end of file diff --git a/neo4j-store/src/main/kotlin/org/ostelco/prime/storage/graph/Neo4jModule.kt b/neo4j-store/src/main/kotlin/org/ostelco/prime/storage/graph/Neo4jModule.kt index 87ec38394..ea8080790 100644 --- a/neo4j-store/src/main/kotlin/org/ostelco/prime/storage/graph/Neo4jModule.kt +++ b/neo4j-store/src/main/kotlin/org/ostelco/prime/storage/graph/Neo4jModule.kt @@ -40,8 +40,11 @@ class Neo4jModule : PrimeModule { data class Config( val host: String, val protocol: String, - val hssNameLookupService: KtsServiceFactory, - val onNewCustomerAction: KtsServiceFactory) + val onNewCustomerAction: KtsServiceFactory, + val allowedRegionsService: KtsServiceFactory, + val onRegionApprovedAction: KtsServiceFactory, + val hssNameLookupService: KtsServiceFactory +) object ConfigRegistry { lateinit var config: Config 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 35b2761d0..e2c0657c0 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 @@ -12,15 +12,16 @@ import arrow.instances.either.monad.monad import org.neo4j.driver.v1.Transaction import org.ostelco.prime.analytics.AnalyticsService import org.ostelco.prime.appnotifier.AppNotifier +import org.ostelco.prime.appnotifier.NotificationType import org.ostelco.prime.auditlog.AuditLog import org.ostelco.prime.dsl.ReadTransaction import org.ostelco.prime.dsl.WriteTransaction import org.ostelco.prime.dsl.forCustomer -import org.ostelco.prime.dsl.forPurchasesBy +import org.ostelco.prime.dsl.forPurchaseBy import org.ostelco.prime.dsl.identifiedBy +import org.ostelco.prime.dsl.linkedToCustomer import org.ostelco.prime.dsl.linkedToRegion import org.ostelco.prime.dsl.linkedToSimProfile -import org.ostelco.prime.dsl.purchasedBy import org.ostelco.prime.dsl.readTransaction import org.ostelco.prime.dsl.referred import org.ostelco.prime.dsl.referredBy @@ -41,8 +42,8 @@ import org.ostelco.prime.model.ChangeSegment import org.ostelco.prime.model.Customer import org.ostelco.prime.model.CustomerRegionStatus import org.ostelco.prime.model.CustomerRegionStatus.APPROVED +import org.ostelco.prime.model.CustomerRegionStatus.AVAILABLE import org.ostelco.prime.model.CustomerRegionStatus.PENDING -import org.ostelco.prime.model.FCMStrings import org.ostelco.prime.model.HasId import org.ostelco.prime.model.KycStatus import org.ostelco.prime.model.KycStatus.REJECTED @@ -104,6 +105,10 @@ import org.ostelco.prime.storage.graph.Graph.read import org.ostelco.prime.storage.graph.Graph.write import org.ostelco.prime.storage.graph.Graph.writeSuspended import org.ostelco.prime.storage.graph.Relation.BELONG_TO_SEGMENT +import org.ostelco.prime.storage.graph.Relation.FOR_PURCHASE_BY +import org.ostelco.prime.storage.graph.Relation.FOR_PURCHASE_OF +import org.ostelco.prime.storage.graph.Relation.HAD_SIM_PROFILE +import org.ostelco.prime.storage.graph.Relation.HAD_SUBSCRIPTION import org.ostelco.prime.storage.graph.Relation.HAS_BUNDLE import org.ostelco.prime.storage.graph.Relation.HAS_SIM_PROFILE import org.ostelco.prime.storage.graph.Relation.HAS_SUBSCRIPTION @@ -111,9 +116,9 @@ import org.ostelco.prime.storage.graph.Relation.IDENTIFIES 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 org.ostelco.prime.storage.graph.model.CustomerRegion +import org.ostelco.prime.storage.graph.model.ExCustomer import org.ostelco.prime.storage.graph.model.Identifies import org.ostelco.prime.storage.graph.model.Identity import org.ostelco.prime.storage.graph.model.Offer @@ -123,6 +128,7 @@ import org.ostelco.prime.storage.graph.model.SimProfile import org.ostelco.prime.storage.graph.model.SubscriptionToBundle import org.ostelco.prime.tracing.Trace import java.time.Instant +import java.time.LocalDate import java.util.* import java.util.stream.Collectors import javax.ws.rs.core.MultivaluedMap @@ -141,15 +147,21 @@ enum class Relation( HAS_SUBSCRIPTION(from = Customer::class, to = Subscription::class), // (Customer) -[HAS_SUBSCRIPTION]-> (Subscription) + HAD_SUBSCRIPTION(from = ExCustomer::class, to = Subscription::class), // (ExCustomer) -[HAD_SUBSCRIPTION]-> (Subscription) + HAS_BUNDLE(from = Customer::class, to = Bundle::class), // (Customer) -[HAS_BUNDLE]-> (Bundle) HAS_SIM_PROFILE(from = Customer::class, to = SimProfile::class), // (Customer) -[HAS_SIM_PROFILE]-> (SimProfile) + HAD_SIM_PROFILE(from = ExCustomer::class, to = SimProfile::class), // (ExCustomer) -[HAD_SIM_PROFILE]-> (SimProfile) + SUBSCRIBES_TO_PLAN(from = Customer::class, to = Plan::class), // (Customer) -[SUBSCRIBES_TO_PLAN]-> (Plan) LINKED_TO_BUNDLE(from = Subscription::class, to = Bundle::class), // (Subscription) -[LINKED_TO_BUNDLE]-> (Bundle) - PURCHASED(from = Customer::class, to = Product::class), // (Customer) -[PURCHASED]-> (Product) + FOR_PURCHASE_BY(from = PurchaseRecord::class, to = Customer::class), // (PurchaseRecord) -[FOR_PURCHASE_BY]-> (Customer) + + FOR_PURCHASE_OF(from = PurchaseRecord::class, to = Product::class), // (PurchaseRecord) -[FOR_PURCHASE_OF]-> (Product) REFERRED(from = Customer::class, to = Customer::class), // (Customer) -[REFERRED]-> (Customer) @@ -163,6 +175,8 @@ enum class Relation( BELONG_TO_REGION(from = Customer::class, to = Region::class), // (Customer) -[BELONG_TO_REGION]-> (Region) + BELONGED_TO_REGION(from = ExCustomer::class, to = Region::class), // (ExCustomer) -[BELONGED_TO_REGION]-> (Region) + SIM_PROFILE_FOR_REGION(from = SimProfile::class, to = Region::class), // (SimProfile) -[SIM_PROFILE_FOR_REGION]-> (Region) SUBSCRIPTION_UNDER_SIM_PROFILE(from = Subscription::class, to = SimProfile::class), // (Subscription) -[SUBSCRIPTION_UNDER_SIM_PROFILE]-> (SimProfile) @@ -184,8 +198,12 @@ object Neo4jStoreSingleton : GraphStore { private val customerEntity = Customer::class.entityType + private val exCustomerEntity = ExCustomer::class.entityType + private val productEntity = Product::class.entityType + private val purchaseRecordEntity = PurchaseRecord::class.entityType + private val subscriptionEntity = Subscription::class.entityType private val bundleEntity = Bundle::class.entityType @@ -216,6 +234,13 @@ object Neo4jStoreSingleton : GraphStore { dataClass = None::class.java) .also { RelationStore(it) } + val exSubscriptionRelation = RelationType( + relation = HAD_SUBSCRIPTION, + from = exCustomerEntity, + to = subscriptionEntity, + dataClass = None::class.java) + .also { RelationStore(it) } + val customerToBundleRelation = RelationType( relation = HAS_BUNDLE, from = customerEntity, @@ -237,15 +262,26 @@ object Neo4jStoreSingleton : GraphStore { dataClass = None::class.java) .also { RelationStore(it) } - val purchaseRecordRelation = RelationType( - relation = PURCHASED, - from = customerEntity, + val exCustomerToSimProfileRelation = RelationType( + relation = HAD_SIM_PROFILE, + from = exCustomerEntity, + to = simProfileEntity, + dataClass = None::class.java) + .also { RelationStore(it) } + + val forPurchaseByRelation = RelationType( + relation = FOR_PURCHASE_BY, + from = purchaseRecordEntity, + to = customerEntity, + dataClass = None::class.java) + .also { UniqueRelationStore(it) } + + val forPurchaseOfRelation = RelationType( + relation = FOR_PURCHASE_OF, + from = purchaseRecordEntity, to = productEntity, - dataClass = PurchaseRecord::class.java) - // TODO vihang there should be 1:1 between relation and store - // NOTE: Keep RelationStore after ChangeableRelationStore - private val changablePurchaseRelationStore = ChangeableRelationStore(purchaseRecordRelation) - private val purchaseRecordRelationStore = RelationStore(purchaseRecordRelation) + dataClass = None::class.java) + .also { UniqueRelationStore(it) } val referredRelation = RelationType( relation = REFERRED, @@ -261,13 +297,20 @@ object Neo4jStoreSingleton : GraphStore { dataClass = PlanSubscription::class.java) private val subscribesToPlanRelationStore = UniqueRelationStore(subscribesToPlanRelation) - private val customerRegionRelation = RelationType( + val customerRegionRelation = RelationType( relation = Relation.BELONG_TO_REGION, from = customerEntity, to = regionEntity, dataClass = CustomerRegion::class.java) private val customerRegionRelationStore = UniqueRelationStore(customerRegionRelation) + val exCustomerRegionRelation = RelationType( + relation = Relation.BELONGED_TO_REGION, + from = exCustomerEntity, + to = regionEntity, + dataClass = None::class.java) + .also { UniqueRelationStore(it) } + val scanInformationRelation = RelationType( relation = Relation.EKYC_SCAN, from = customerEntity, @@ -289,8 +332,10 @@ object Neo4jStoreSingleton : GraphStore { dataClass = None::class.java) .also { UniqueRelationStore(it) } - private val hssNameLookup: HssNameLookupService = config.hssNameLookupService.getKtsService() private val onNewCustomerAction: OnNewCustomerAction = config.onNewCustomerAction.getKtsService() + private val allowedRegionsService: AllowedRegionsService = config.allowedRegionsService.getKtsService() + private val onRegionApprovedAction: OnRegionApprovedAction = config.onRegionApprovedAction.getKtsService() + private val hssNameLookup: HssNameLookupService = config.hssNameLookupService.getKtsService() // ------------- // Client Store @@ -368,7 +413,7 @@ object Neo4jStoreSingleton : GraphStore { if (referredBy != null) { fact { (Customer withId referredBy) referred (Customer withId customer.id) }.bind() } - onNewCustomerAction.apply(identity = identity, customerId = customer.id, transaction = transaction).bind() + onNewCustomerAction.apply(identity = identity, customer = customer, transaction = transaction).bind() AuditLog.info(customerId = customer.id, message = "Customer is created") }.fix() }.unsafeRunSync() @@ -393,19 +438,55 @@ object Neo4jStoreSingleton : GraphStore { .ifFailedThenRollback(transaction) } - // TODO vihang: When we read and then delete, it fails when deserialization does not work. override fun removeCustomer(identity: org.ostelco.prime.model.Identity): Either = writeTransaction { - write(query = """ - MATCH (i:${identityEntity.name} {id:'${identity.id}'})-[:${identifiesRelation.name}]->(c:${customerEntity.name}) - OPTIONAL MATCH (c)-[:${customerToBundleRelation.name}]->(b:${bundleEntity.name}) - OPTIONAL MATCH (c)-[:${scanInformationRelation.name}]->(s:${scanInformationEntity.name}) - DETACH DELETE i, c, b, s; - """.trimIndent(), transaction = transaction) { statementResult -> - Either.cond( - test = statementResult.summary().counters().nodesDeleted() > 0, - ifTrue = {}, - ifFalse = { NotFoundError(type = identityEntity.name, id = identity.id) }) - } + IO { + Either.monad().binding { + // get customer id + val customerId = getCustomerId(identity).bind() + // create ex-customer with same id + create { ExCustomer(id = customerId, terminationDate = LocalDate.now().toString()) }.bind() + // get all subscriptions and link them to ex-customer + val subscriptions = get(Subscription subscribedBy (Customer withId customerId)).bind() + for (subscription in subscriptions) { + fact { (ExCustomer withId customerId) subscribedTo (Subscription withMsisdn subscription.msisdn) }.bind() + } + // get all SIM profiles and link them to ex-customer. + val simProfiles = get(SimProfile forCustomer (Customer withId customerId)).bind() + val simProfileRegions = mutableSetOf() + for (simProfile in simProfiles) { + fact { (ExCustomer withId customerId) had (SimProfile withId simProfile.id) }.bind() + // also get regions linked to those SimProfiles. + simProfileRegions.addAll(get(Region linkedToSimProfile (SimProfile withId simProfile.id)).bind()) + } + // get Regions linked to Customer + val regions = get(Region linkedToCustomer (Customer withId customerId)).bind() + // TODO vihang: clear eKYC data for Regions without any SimProfile +// val regionsWithoutSimProfile = regions - simProfileRegions +// // Link regions with SIM profiles to ExCustomer +// for (region in simProfileRegions) { +// fact { (ExCustomer withId customerId) belongedTo (Region withCode region.id) }.bind() +// } + // (For now) Link regions to ExCustomer + for (region in regions) { + fact { (ExCustomer withId customerId) belongedTo (Region withCode region.id) }.bind() + } + + // TODO vihang: When we read and then delete, it fails when deserialization does not work. + write(query = """ + MATCH (i:${identityEntity.name} {id:'${identity.id}'})-[:${identifiesRelation.name}]->(c:${customerEntity.name}) + OPTIONAL MATCH (c)-[:${customerToBundleRelation.name}]->(b:${bundleEntity.name}) + OPTIONAL MATCH (c)<-[:${forPurchaseByRelation.name}]-(pr:${purchaseRecordEntity.name}) + OPTIONAL MATCH (c)-[:${scanInformationRelation.name}]->(s:${scanInformationEntity.name}) + DETACH DELETE i, c, b, pr, s; + """.trimIndent(), transaction = transaction) { statementResult -> + Either.cond( + test = statementResult.summary().counters().nodesDeleted() > 0, + ifTrue = {}, + ifFalse = { NotFoundError(type = identityEntity.name, id = identity.id) }) + }.bind() + }.fix() + }.unsafeRunSync() + .ifFailedThenRollback(transaction) } // @@ -415,9 +496,14 @@ object Neo4jStoreSingleton : GraphStore { override fun getAllRegionDetails(identity: org.ostelco.prime.model.Identity): Either> = readTransaction { getCustomerId(identity = identity) .flatMap { customerId -> - getRegionDetails( - customerId = customerId, - transaction = transaction).right() + getAllowedRegionIds(identity, transaction).map { allowedIds -> + val allRegions = getAvailableRegionDetails(transaction) + val customerRegions = getRegionDetails( + customerId = customerId, + transaction = transaction) + combineRegions(allRegions, customerRegions) + .filter { allowedIds.contains(it.region.id) } + } } } @@ -427,16 +513,24 @@ object Neo4jStoreSingleton : GraphStore { getCustomerId(identity = identity) .flatMap { customerId -> - getRegionDetails( - customerId = customerId, - regionCode = regionCode, - transaction = transaction) - .singleOrNull() - ?.right() - ?: NotFoundError(type = customerRegionRelation.name, id = "$customerId -> $regionCode").left() + getAllowedRegionIds(identity, transaction).flatMap { allowedIds -> + getRegionDetails( + customerId = customerId, + regionCode = regionCode, + transaction = transaction).singleOrNull { allowedIds.contains(it.region.id) } + ?.right() + ?: NotFoundError(type = customerRegionRelation.name, id = "$customerId -> $regionCode").left() + } } } + // Retrieve the list of allowed region Ids from AllowedRegionsService + private fun getAllowedRegionIds( + identity: org.ostelco.prime.model.Identity, + transaction: PrimeTransaction): Either> = getCustomer(identity).flatMap { + allowedRegionsService.get(identity, it, transaction) + } + private fun getRegionDetails( customerId: String, regionCode: String? = null, @@ -498,6 +592,31 @@ object Neo4jStoreSingleton : GraphStore { } } + private fun getAvailableRegionDetails(transaction: Transaction): Collection { + // Make list of details using regions in present in graphDB with default values + val query = "MATCH (r:${regionEntity.name}) RETURN r;" + return read(query, transaction) { it -> + it.list { record -> + val region = regionEntity.createEntity(record["r"].asMap()) + RegionDetails( + region = region, + status = AVAILABLE, + kycStatusMap = getKycStatusMapForRegion(region.id.toLowerCase()), + simProfiles = emptyList()) + }.requireNoNulls() + } + } + + private fun combineRegions(allRegions: Collection, customerRegions: Collection): Collection { + // Create a map with default region details + val combined = allRegions.associateBy { it.region.id }.toMutableMap() + // Overwrite default region details with items from actual region-relations for customer + customerRegions.forEach { + combined[it.region.id] = it + } + return combined.values + } + // // SIM Profile // @@ -610,7 +729,7 @@ object Neo4jStoreSingleton : GraphStore { fact { (Customer withId customerId) subscribesTo (Subscription withMsisdn msisdn) }.bind() fact { (Subscription withMsisdn msisdn) isUnder (SimProfile withId simProfile.id) }.bind() } - if (profileType != "android") { + if (!setOf("android", "iphone", "test").contains(profileType)) { emailNotifier.sendESimQrCodeEmail( email = customer.contactEmail, name = customer.nickname, @@ -882,7 +1001,7 @@ object Neo4jStoreSingleton : GraphStore { -[:${customerToSegmentRelation.name}]->(:${segmentEntity.name}) <-[:${offerToSegmentRelation.name}]-(:${offerEntity.name}) -[:${offerToProductRelation.name}]->(product:${productEntity.name}) - RETURN product; + RETURN DISTINCT product; """.trimIndent(), transaction) { statementResult -> Either.right(statementResult @@ -1041,7 +1160,7 @@ object Neo4jStoreSingleton : GraphStore { /* If this step fails, the previously added 'removeInvoice' call added to the transaction will ensure that the invoice will be voided. */ - createPurchaseRecordRelation(customer.id, purchaseRecord) + createPurchaseRecord(customer.id, purchaseRecord) .mapLeft { logger.error("Failed to save purchase record for customer ${customer.id}, invoice-id $invoiceId, invoice will be voided in Stripe") AuditLog.error(customerId = customer.id, message = "Failed to save purchase record - invoice-id $invoiceId, invoice will be voided in Stripe") @@ -1139,9 +1258,9 @@ object Neo4jStoreSingleton : GraphStore { /* Bail out if subscriber tries to buy an already bought plan. Note: Already verified above that 'customer' (subscriber) exists. */ - get(Product purchasedBy (Customer withId customer.id)) - .map { products -> - if (products.any { x -> x.sku == sku }) { + get(PurchaseRecord forPurchaseBy (Customer withId customer.id)) + .map { purchaseRecords -> + if (purchaseRecords.any { x:PurchaseRecord -> x.product.sku == sku }) { PlanAlredyPurchasedError("A subscription to plan $sku already exists") .left().bind() } @@ -1397,46 +1516,40 @@ object Neo4jStoreSingleton : GraphStore { readTransaction { getCustomerId(identity = identity) .flatMap { customerId -> - get(PurchaseRecord forPurchasesBy (Customer withId customerId)) + get(PurchaseRecord forPurchaseBy (Customer withId customerId)) } } override fun addPurchaseRecord(customerId: String, purchase: PurchaseRecord): Either = writeTransaction { - createPurchaseRecordRelation(customerId, purchase) + createPurchaseRecord(customerId, purchase) .ifFailedThenRollback(transaction) } - fun WriteTransaction.createPurchaseRecordRelation(customerId: String, - purchaseRecord: PurchaseRecord): Either { + fun WriteTransaction.createPurchaseRecord(customerId: String, + purchaseRecord: PurchaseRecord): Either { val invoiceId = purchaseRecord.properties["invoiceId"] - /* Avoid charging for the same invoice twice if invoice information - is present. */ - return if (invoiceId != null) { - getPurchaseRecordUsingInvoiceId(customerId, invoiceId) - .fold({ - get(Customer withId customerId).flatMap { customer -> - get(Product withSku purchaseRecord.product.sku).flatMap { product -> - fact { (Customer withId customerId) purchased (Product withSku product.sku) using purchaseRecord } - .map { purchaseRecord.id } - } - } - }, { - ValidationError(type = purchaseRecordRelation.name, - id = purchaseRecord.id, - message = "A purchase record for ${purchaseRecord.product} for customer $customerId already exists") - .left() - }) - } else { - get(Customer withId customerId).flatMap { customer -> - get(Product withSku purchaseRecord.product.sku).flatMap { product -> - fact { (Customer withId customerId) purchased (Product withSku product.sku) using purchaseRecord } - .map { purchaseRecord.id } + return IO { + Either.monad().binding { + + if (invoiceId != null + && getPurchaseRecordUsingInvoiceId(customerId, invoiceId).isRight()) { + /* Avoid charging for the same invoice twice if invoice information is present. */ + + ValidationError(type = purchaseRecordEntity.name, + id = purchaseRecord.id, + message = "A purchase record for ${purchaseRecord.product} for customer $customerId already exists") + .left() + .bind() } - } - } + create { purchaseRecord }.bind() + fact { (PurchaseRecord withId purchaseRecord.id) forPurchaseBy (Customer withId customerId) }.bind() + fact { (PurchaseRecord withId purchaseRecord.id) forPurchaseOf (Product withSku purchaseRecord.product.sku) }.bind() + purchaseRecord.id + }.fix() + }.unsafeRunSync() } /* As Stripes invoice-id is used as the 'id' of a purchase record, this method @@ -1445,12 +1558,12 @@ object Neo4jStoreSingleton : GraphStore { customerId: String, invoiceId: String): Either = - get(PurchaseRecord forPurchasesBy (Customer withId customerId)) + get(PurchaseRecord forPurchaseBy (Customer withId customerId)) .map { records -> records.find { it.properties["invoiceId"] == invoiceId } - }.leftIfNull { NotFoundError(type = purchaseRecordRelation.name, id = invoiceId) } + }.leftIfNull { NotFoundError(type = purchaseRecordEntity.name, id = invoiceId) } // // Referrals @@ -1473,26 +1586,26 @@ object Neo4jStoreSingleton : GraphStore { } internal fun createCustomerRegionSetting( - customerId: String, + customer: Customer, status: CustomerRegionStatus, regionCode: String): Either = writeTransaction { createCustomerRegionSetting( - customerId = customerId, + customer = customer, status = status, regionCode = regionCode, transaction = transaction) } private fun createCustomerRegionSetting( - customerId: String, + customer: Customer, status: CustomerRegionStatus, regionCode: String, transaction: PrimeTransaction): Either = customerRegionRelationStore .createIfAbsent( - fromId = customerId, + fromId = customer.id, relation = CustomerRegion( status = status, kycStatusMap = getKycStatusMapForRegion(regionCode)), @@ -1500,10 +1613,11 @@ object Neo4jStoreSingleton : GraphStore { transaction = transaction) .flatMap { if (status == APPROVED) { - assignCustomerToSegment( - customerId = customerId, - segmentId = getInitialSegmentNameForRegion(regionCode, transaction), - transaction = transaction) + onRegionApprovedAction.apply( + customer = customer, + regionCode = regionCode, + transaction = PrimeTransaction(transaction) + ) } else { Unit.right() } @@ -1537,29 +1651,29 @@ object Neo4jStoreSingleton : GraphStore { identity: org.ostelco.prime.model.Identity, regionCode: String): Either = writeTransaction { - getCustomerId(identity = identity) - .flatMap { customerId -> + getCustomer(identity = identity) + .flatMap { customer -> // Generate new id for the scan val scanId = UUID.randomUUID().toString() val newScan = ScanInformation(scanId = scanId, countryCode = regionCode, status = ScanStatus.PENDING, scanResult = null) createCustomerRegionSetting( - customerId = customerId, status = PENDING, regionCode = regionCode.toLowerCase(), transaction = transaction) + customer = customer, status = PENDING, regionCode = regionCode.toLowerCase(), transaction = transaction) .flatMap { create { newScan } } .flatMap { - scanInformationRelationStore.createIfAbsent(customerId, newScan.id, transaction) + scanInformationRelationStore.createIfAbsent(customer.id, newScan.id, transaction) } .flatMap { setKycStatus( - customerId = customerId, + customer = customer, regionCode = regionCode.toLowerCase(), kycType = JUMIO, kycStatus = KycStatus.PENDING, transaction = transaction) } .map { - AuditLog.info(customerId = customerId, message = "Created new Jumio scan id - ${newScan.id}") + AuditLog.info(customerId = customer.id, message = "Created new Jumio scan id - ${newScan.id}") newScan } } @@ -1619,14 +1733,13 @@ object Neo4jStoreSingleton : GraphStore { scanInformationDatastore.upsertVendorScanInformation(customer.id, scanInformation.countryCode, vendorData) .flatMap { appNotifier.notify( + notificationType = NotificationType.JUMIO_VERIFICATION_SUCCEEDED, customerId = customer.id, - title = FCMStrings.NOTIFICATION_TITLE.s, - body = FCMStrings.JUMIO_IDENTITY_VERIFIED.s, data = extendedStatus ) logger.info(NOTIFY_OPS_MARKER, "Jumio verification succeeded for ${customer.contactEmail} Info: $extendedStatus") setKycStatus( - customerId = customer.id, + customer = customer, regionCode = scanInformation.countryCode.toLowerCase(), kycType = JUMIO, transaction = transaction) @@ -1634,14 +1747,13 @@ object Neo4jStoreSingleton : GraphStore { } else { // TODO: find out what more information can be passed to the client. appNotifier.notify( + notificationType = NotificationType.JUMIO_VERIFICATION_FAILED, customerId = customer.id, - title = FCMStrings.NOTIFICATION_TITLE.s, - body = FCMStrings.JUMIO_IDENTITY_FAILED.s, data = extendedStatus ) logger.info(NOTIFY_OPS_MARKER, "Jumio verification failed for ${customer.contactEmail} Info: $extendedStatus") setKycStatus( - customerId = customer.id, + customer = customer, regionCode = scanInformation.countryCode.toLowerCase(), kycType = JUMIO, kycStatus = REJECTED, @@ -1667,11 +1779,11 @@ object Neo4jStoreSingleton : GraphStore { return IO { Either.monad().binding { - val customerId = getCustomer(identity = identity).bind().id + val customer = getCustomer(identity = identity).bind() // set MY_INFO KYC Status to Pending setKycStatus( - customerId = customerId, + customer = customer, regionCode = "sg", kycType = MY_INFO, kycStatus = KycStatus.PENDING).bind() @@ -1685,9 +1797,9 @@ object Neo4jStoreSingleton : GraphStore { logger.error("Failed to fetched MyInfo $version using authCode = $authorisationCode", e) null } ?: SystemError( - type = "MyInfo Auth Code", - id = authorisationCode, - message = "Failed to fetched MyInfo $version").left().bind() + type = "MyInfo Auth Code", + id = authorisationCode, + message = "Failed to fetched MyInfo $version").left().bind() // TODO vihang: Should we set status for NRIC_FIN to APPROVED? @@ -1703,7 +1815,7 @@ object Neo4jStoreSingleton : GraphStore { message = "Failed to fetched MyInfo $version").left().bind() secureArchiveService.archiveEncrypted( - customerId = customerId, + customerId = customer.id, fileName = "myInfoData", regionCodes = listOf("sg"), dataMap = mapOf( @@ -1714,7 +1826,7 @@ object Neo4jStoreSingleton : GraphStore { // set MY_INFO KYC Status to Approved setKycStatus( - customerId = customerId, + customer = customer, regionCode = "sg", kycType = MY_INFO).bind() @@ -1738,11 +1850,11 @@ object Neo4jStoreSingleton : GraphStore { logger.info("checkNricFinIdUsingDave for $nricFinId") - val customerId = getCustomer(identity = identity).bind().id + val customer = getCustomer(identity = identity).bind() // set NRIC_FIN KYC Status to Pending setKycStatus( - customerId = customerId, + customer = customer, regionCode = "sg", kycType = NRIC_FIN, kycStatus = KycStatus.PENDING).bind() @@ -1755,7 +1867,7 @@ object Neo4jStoreSingleton : GraphStore { } secureArchiveService.archiveEncrypted( - customerId = customerId, + customerId = customer.id, fileName = "nricFin", regionCodes = listOf("sg"), dataMap = mapOf("nricFinId" to nricFinId.toByteArray()) @@ -1763,7 +1875,7 @@ object Neo4jStoreSingleton : GraphStore { // set NRIC_FIN KYC Status to Approved setKycStatus( - customerId = customerId, + customer = customer, regionCode = "sg", kycType = NRIC_FIN).bind() }.fix() @@ -1775,32 +1887,33 @@ object Neo4jStoreSingleton : GraphStore { // override fun saveAddress( identity: org.ostelco.prime.model.Identity, - address: String): Either { + address: String, + regionCode: String): Either { return IO { Either.monad().binding { - val customerId = getCustomer(identity = identity).bind().id + val customer = getCustomer(identity = identity).bind() - // set // apply(from = "../../gradle/jacoco.gradle") KYC Status to Pending + // set ADDRESS KYC Status to Pending setKycStatus( - customerId = customerId, - regionCode = "sg", + customer = customer, + regionCode = regionCode, kycType = ADDRESS, kycStatus = KycStatus.PENDING).bind() secureArchiveService.archiveEncrypted( - customerId = customerId, + customerId = customer.id, fileName = "address", - regionCodes = listOf("sg"), + regionCodes = listOf(regionCode), dataMap = mapOf( "address" to address.toByteArray()) ).bind() - // set // apply(from = "../../gradle/jacoco.gradle") KYC Status to Approved + // set ADDRESS KYC Status to Approved setKycStatus( - customerId = customerId, - regionCode = "sg", + customer = customer, + regionCode = regionCode, kycType = ADDRESS).bind() }.fix() }.unsafeRunSync() @@ -1811,13 +1924,13 @@ object Neo4jStoreSingleton : GraphStore { // internal fun setKycStatus( - customerId: String, + customer: Customer, regionCode: String, kycType: KycType, kycStatus: KycStatus = KycStatus.APPROVED) = writeTransaction { setKycStatus( - customerId = customerId, + customer = customer, regionCode = regionCode, kycType = kycType, kycStatus = kycStatus, @@ -1828,7 +1941,7 @@ object Neo4jStoreSingleton : GraphStore { // FIXME: vihang This implementation has risk of loss of data due during concurrency to stale read since it does // READ-UPDATE-WRITE. private fun setKycStatus( - customerId: String, + customer: Customer, regionCode: String, kycType: KycType, kycStatus: KycStatus = KycStatus.APPROVED, @@ -1840,14 +1953,14 @@ object Neo4jStoreSingleton : GraphStore { val approvedKycTypeSetList = getApprovedKycTypeSetList(regionCode) val existingCustomerRegion = customerRegionRelationStore.get( - fromId = customerId, + fromId = customer.id, toId = regionCode, transaction = transaction) .getOrElse { CustomerRegion(status = PENDING, kycStatusMap = getKycStatusMapForRegion(regionCode)) } val existingKycStatusMap = existingCustomerRegion.kycStatusMap val existingKycStatus = existingKycStatusMap[kycType] - val newKycStatus = when(existingKycStatus) { + val newKycStatus = when (existingKycStatus) { // APPROVED is end state. No more state change. KycStatus.APPROVED -> KycStatus.APPROVED // REJECTED and PENDING to 'any' is allowed @@ -1856,12 +1969,12 @@ object Neo4jStoreSingleton : GraphStore { if (existingKycStatus != newKycStatus) { if (kycStatus == newKycStatus) { - AuditLog.info(customerId = customerId, message = "Setting $kycType status from $existingKycStatus to $newKycStatus") + AuditLog.info(customerId = customer.id, message = "Setting $kycType status from $existingKycStatus to $newKycStatus") } else { - AuditLog.info(customerId = customerId, message = "Setting $kycType status from $existingKycStatus to $newKycStatus instead of $kycStatus") + AuditLog.info(customerId = customer.id, message = "Setting $kycType status from $existingKycStatus to $newKycStatus instead of $kycStatus") } } else { - AuditLog.info(customerId = customerId, message = "Ignoring setting $kycType status to $kycStatus since it is already $existingKycStatus") + AuditLog.info(customerId = customer.id, message = "Ignoring setting $kycType status to $kycStatus since it is already $existingKycStatus") } val newKycStatusMap = existingKycStatusMap.copy(key = kycType, value = newKycStatus) @@ -1879,16 +1992,17 @@ object Neo4jStoreSingleton : GraphStore { } if (approvedNow) { - AuditLog.info(customerId = customerId, message = "Approved for region - $regionCode") - assignCustomerToSegment( - customerId = customerId, - segmentId = getInitialSegmentNameForRegion(regionCode, transaction), - transaction = transaction).bind() + AuditLog.info(customerId = customer.id, message = "Approved for region - $regionCode") + onRegionApprovedAction.apply( + customer = customer, + regionCode = regionCode, + transaction = PrimeTransaction(transaction) + ).bind() } customerRegionRelationStore .createOrUpdate( - fromId = customerId, + fromId = customer.id, relation = CustomerRegion(status = newStatus, kycStatusMap = newKycStatusMap), toId = regionCode, transaction = transaction) @@ -1901,29 +2015,22 @@ object Neo4jStoreSingleton : GraphStore { private fun getKycStatusMapForRegion(regionCode: String): Map { return when (regionCode) { "sg" -> setOf(JUMIO, MY_INFO, NRIC_FIN, ADDRESS) + "my" -> setOf(JUMIO, ADDRESS) else -> setOf(JUMIO) }.map { it to KycStatus.PENDING }.toMap() } private fun getApprovedKycTypeSetList(regionCode: String): List> { return when (regionCode) { - "sg" -> listOf(setOf(MY_INFO, ADDRESS), - setOf(JUMIO, ADDRESS)) + "sg" -> listOf( + setOf(MY_INFO, ADDRESS), + setOf(JUMIO, ADDRESS) + ) + "my" -> listOf(setOf(JUMIO, ADDRESS)) else -> listOf(setOf(JUMIO)) } } - private fun getInitialSegmentNameForRegion(regionCode: String, transaction: Transaction): String = - segmentStore.get(getPlanSegmentNameFromCountryCode(regionCode), transaction) - .fold( - { - getSegmentNameFromCountryCode(regionCode) - }, - { - it.id - } - ) - // ------------ // Admin Store // ------------ @@ -1963,19 +2070,40 @@ object Neo4jStoreSingleton : GraphStore { } - override fun getIdentityForContactEmail(contactEmail: String): Either = readTransaction { + override fun getIdentityForCustomerId(id: String): Either = readTransaction { read(""" - MATCH (:${customerEntity.name} { contactEmail:'$contactEmail' })<-[r:${identifiesRelation.name}]-(identity:${identityEntity.name}) + MATCH (:${customerEntity.name} { id:'$id' })<-[r:${identifiesRelation.name}]-(identity:${identityEntity.name}) RETURN identity, r.provider as provider """.trimIndent(), transaction) { if (it.hasNext()) { - val record = it.single() + val record = it.list().first() val identity = identityEntity.createEntity(record.get("identity").asMap()) val provider = record.get("provider").asString() Either.right(ModelIdentity(id = identity.id, type = identity.type, provider = provider)) } else { - Either.left(NotFoundError(type = customerEntity.name, id = contactEmail)) + Either.left(NotFoundError(type = customerEntity.name, id = id)) + } + } + } + + override fun getIdentitiesFor(queryString: String): Either> = readTransaction { + read(""" + MATCH (c:${customerEntity.name})<-[r:${identifiesRelation.name}]-(identity:${identityEntity.name}) + WHERE c.contactEmail contains '$queryString' or c.nickname contains '$queryString' or c.id contains '$queryString' + RETURN c, identity, r.provider as provider + """.trimIndent(), + transaction) { + if (it.hasNext()) { + val identityList = mutableListOf() + it.forEach { record -> + val identity = identityEntity.createEntity(record.get("identity").asMap()) + val provider = record.get("provider").asString() + identityList.add(ModelIdentity(id = identity.id, type = identity.type, provider = provider)) + } + Either.right(identityList) + } else { + Either.left(NotFoundError(type = customerEntity.name, id = queryString)) } } } @@ -2006,8 +2134,8 @@ object Neo4jStoreSingleton : GraphStore { override fun getPaidCustomerCount(): Long = readTransaction { read(""" - MATCH (customer:${customerEntity.name})-[:${purchaseRecordRelation.name}]->(product:${productEntity.name}) - WHERE product.`price/amount` > 0 + MATCH (customer:${customerEntity.name})<-[:${forPurchaseByRelation.name}]-(purchaseRecord:${purchaseRecordEntity.name}) + WHERE purchaseRecord.`product/price/amount` > 0 RETURN count(customer) AS count """.trimIndent(), transaction) { result -> result.single().get("count").asLong() @@ -2206,9 +2334,10 @@ object Neo4jStoreSingleton : GraphStore { ) /* Will exit if an existing purchase record matches on 'invoiceId'. */ - createPurchaseRecordRelation(customerId, purchaseRecord) + createPurchaseRecord(customerId, purchaseRecord) .bind() + // FIXME Moving customer to new segments should be done only based on productClass. /* Offer products to the newly signed up subscriber. */ product.segmentIds.forEach { segmentId -> assignCustomerToSegment( @@ -2234,14 +2363,12 @@ object Neo4jStoreSingleton : GraphStore { override fun getPurchaseTransactions(start: Long, end: Long): Either> = readTransaction { read(""" - MATCH(c)-[r:PURCHASED]->(p) WHERE r.timestamp >= '${start}' AND r.timestamp <= '${end}' - RETURN r + MATCH(pr:${purchaseRecordEntity.name})-[:${forPurchaseOfRelation.name}]->(:${productEntity.name}) + WHERE toInteger(pr.timestamp) >= ${start} AND toInteger(pr.timestamp) <= ${end} AND toInteger(pr.`product/price/amount`) > 0 + RETURN pr """.trimIndent(), transaction) { statementResult -> statementResult.list { record -> - purchaseRecordRelation.createRelation(record["r"].asMap()) - }.filter { - /* Exclude free products. */ - it.product.price.amount > 0 + purchaseRecordEntity.createEntity(record["pr"].asMap()) }.right() } } @@ -2281,8 +2408,7 @@ object Neo4jStoreSingleton : GraphStore { "amount" to it.product.price.amount, "currency" to it.product.price.currency.toLowerCase(), "refunded" to (it.refund != null), - "created" to it.timestamp, - "properties" to it.properties) + "created" to it.timestamp) }.plus( paymentRecords.map { mapOf("type" to "paymentRecord", @@ -2290,8 +2416,7 @@ object Neo4jStoreSingleton : GraphStore { "amount" to it.amount, "currency" to it.currency, /* (Stripe) Always lower case. */ "refunded" to it.refunded, - "created" to it.created, - "properties" to it.properties) + "created" to it.created) } ).groupBy { it["chargeId"].hashCode() + it["amount"].hashCode() + @@ -2347,10 +2472,6 @@ object Neo4jStoreSingleton : GraphStore { return Unit.right() } - private fun updatePurchaseRecord( - purchase: PurchaseRecord, - primeTransaction: PrimeTransaction): Either = changablePurchaseRelationStore.update(purchase, primeTransaction) - override fun refundPurchase( identity: org.ostelco.prime.model.Identity, purchaseRecordId: String, @@ -2363,7 +2484,7 @@ object Neo4jStoreSingleton : GraphStore { NotFoundPaymentError("Failed to find customer with identity - $identity", error = it) }.bind() - val purchaseRecord = changablePurchaseRelationStore.get(purchaseRecordId, transaction) + val purchaseRecord = get(PurchaseRecord withId purchaseRecordId) // If we can't find the record, return not-found .mapLeft { org.ostelco.prime.paymentprocessor.core.NotFoundError("Purchase Record unavailable", @@ -2381,7 +2502,7 @@ object Neo4jStoreSingleton : GraphStore { val changedPurchaseRecord = purchaseRecord.copy( refund = refund ) - updatePurchaseRecord(changedPurchaseRecord, transaction) + update { changedPurchaseRecord } .mapLeft { logger.error("failed to update purchase record, for refund $refund.id, chargeId $purchaseRecordId, payment has been refunded in Stripe") BadGatewayError("Failed to update purchase record for refund ${refund.id}", @@ -2407,7 +2528,6 @@ object Neo4jStoreSingleton : GraphStore { private val offerEntity = Offer::class.entityType private val segmentEntity = Segment::class.entityType - private val segmentStore = Segment::class.entityStore private val offerToSegmentRelation = RelationType(OFFERED_TO_SEGMENT, offerEntity, segmentEntity, None::class.java) private val offerToSegmentStore = RelationStore(offerToSegmentRelation) @@ -2415,7 +2535,7 @@ object Neo4jStoreSingleton : GraphStore { private val offerToProductRelation = RelationType(OFFER_HAS_PRODUCT, offerEntity, productEntity, None::class.java) private val offerToProductStore = RelationStore(offerToProductRelation) - private val customerToSegmentRelation = RelationType(BELONG_TO_SEGMENT, customerEntity, segmentEntity, None::class.java) + val customerToSegmentRelation = RelationType(BELONG_TO_SEGMENT, customerEntity, segmentEntity, None::class.java) private val customerToSegmentStore = RelationStore(customerToSegmentRelation) // diff --git a/neo4j-store/src/main/kotlin/org/ostelco/prime/storage/graph/Schema.kt b/neo4j-store/src/main/kotlin/org/ostelco/prime/storage/graph/Schema.kt index 224d4b2ff..0768af777 100644 --- a/neo4j-store/src/main/kotlin/org/ostelco/prime/storage/graph/Schema.kt +++ b/neo4j-store/src/main/kotlin/org/ostelco/prime/storage/graph/Schema.kt @@ -314,37 +314,6 @@ class RelationStore(private val relationType } } -class ChangeableRelationStore(private val relationType: RelationType) : BaseRelationStore() { - - init { - relationType.relationStore = this - } - - fun get(id: String, transaction: Transaction): Either { - return read("""MATCH (from)-[r:${relationType.name}{id:'$id'}]->(to) RETURN r;""", - transaction) { statementResult -> - if (statementResult.hasNext()) { - relationType.createRelation(statementResult.single().get("r").asMap()).right() - } else { - Either.left(NotFoundError(type = relationType.name, id = id)) - } - } - } - - fun update(relation: RELATION, transaction: Transaction): Either { - val properties = getProperties(relation) - // TODO vihang: replace setClause with map based settings written by Kjell - val setClause: String = properties.entries.fold("") { acc, entry -> """$acc SET r.`${entry.key}` = "${entry.value}" """ } - return write("""MATCH (from)-[r:${relationType.name}{id:'${relation.id}'}]->(to) $setClause ;""", - transaction) { statementResult -> - Either.cond( - test = statementResult.summary().counters().containsUpdates(), // TODO vihang: this is not perfect way to check if updates are applied - ifTrue = {}, - ifFalse = { NotUpdatedError(type = relationType.name, id = relation.id) }) - } - } -} - // Removes double apostrophes from key values in a JSON string. // Usage: output = re.replace(input, "$1$2$3") val re = Regex("""([,{])\s*"([^"]+)"\s*(:)""") diff --git a/neo4j-store/src/main/kotlin/org/ostelco/prime/storage/graph/Util.kt b/neo4j-store/src/main/kotlin/org/ostelco/prime/storage/graph/Util.kt index 9bc37f50f..ecb007b58 100644 --- a/neo4j-store/src/main/kotlin/org/ostelco/prime/storage/graph/Util.kt +++ b/neo4j-store/src/main/kotlin/org/ostelco/prime/storage/graph/Util.kt @@ -12,12 +12,6 @@ import java.text.DecimalFormatSymbols val adminStore by lazy { getResource() } -// Helper for naming of default segments based on country code. -fun getSegmentNameFromCountryCode(countryCode: String): String = "country-$countryCode".toLowerCase() - -// Helper for naming of default plan segments based on country code. -fun getPlanSegmentNameFromCountryCode(countryCode: String): String = "plan-country-$countryCode".toLowerCase() - private val dfs = DecimalFormatSymbols().apply { groupingSeparator = '_' } diff --git a/neo4j-store/src/main/kotlin/org/ostelco/prime/storage/graph/model/Model.kt b/neo4j-store/src/main/kotlin/org/ostelco/prime/storage/graph/model/Model.kt index 858754af2..c1c6a1c26 100644 --- a/neo4j-store/src/main/kotlin/org/ostelco/prime/storage/graph/model/Model.kt +++ b/neo4j-store/src/main/kotlin/org/ostelco/prime/storage/graph/model/Model.kt @@ -33,6 +33,15 @@ data class SimProfile( companion object } -data class Segment(override val id: String) : HasId +data class Segment(override val id: String) : HasId { + companion object +} + +data class Offer(override val id: String) : HasId -data class Offer(override val id: String) : HasId \ No newline at end of file +data class ExCustomer( + override val id:String, + val terminationDate: String) : HasId { + + companion object +} diff --git a/neo4j-store/src/main/resources/AcceptanceTestSetup.kts b/neo4j-store/src/main/resources/AcceptanceTestSetup.kts index 9e0772cb8..a77f2fef6 100644 --- a/neo4j-store/src/main/resources/AcceptanceTestSetup.kts +++ b/neo4j-store/src/main/resources/AcceptanceTestSetup.kts @@ -19,7 +19,6 @@ import org.ostelco.prime.storage.graph.Neo4jStoreSingleton.atomicCreateOffer import org.ostelco.prime.storage.graph.Neo4jStoreSingleton.createIndex import org.ostelco.prime.storage.graph.adminStore import org.ostelco.prime.storage.graph.createProduct -import org.ostelco.prime.storage.graph.getSegmentNameFromCountryCode private val logger by getLogger() @@ -60,7 +59,7 @@ job { atomicCreateOffer( offer = Offer(id = "default_offer"), - segments = listOf(Segment(id = getSegmentNameFromCountryCode("NO"))), + segments = listOf(Segment(id = "country-no")), products = listOf( createProduct(sku = "1GB_249NOK"), createProduct(sku = "2GB_299NOK"), diff --git a/neo4j-store/src/main/resources/AllowedRegionsService.kts b/neo4j-store/src/main/resources/AllowedRegionsService.kts new file mode 100644 index 000000000..07a657380 --- /dev/null +++ b/neo4j-store/src/main/resources/AllowedRegionsService.kts @@ -0,0 +1,18 @@ +import arrow.core.Either +import arrow.core.right +import org.ostelco.prime.model.Customer +import org.ostelco.prime.model.Identity +import org.ostelco.prime.storage.StoreError +import org.ostelco.prime.storage.graph.AllowedRegionsService +import org.ostelco.prime.storage.graph.PrimeTransaction + +object : AllowedRegionsService { + override fun get(identity: Identity, customer: Customer, transaction: PrimeTransaction): Either> { + val allowedEmailDomains = listOf("@bar.com", "@redotter.sg", "@test.com") + val matchedDomains = allowedEmailDomains.filter { customer.contactEmail.toLowerCase().endsWith(it) } + return if (matchedDomains.size > 0) + listOf("no", "sg", "us", "my").right() + else + listOf("us", "my").right() + } +} \ No newline at end of file diff --git a/neo4j-store/src/main/resources/IntegrationTestSetup.kts b/neo4j-store/src/main/resources/IntegrationTestSetup.kts index 2f2199307..2b1f40b8a 100644 --- a/neo4j-store/src/main/resources/IntegrationTestSetup.kts +++ b/neo4j-store/src/main/resources/IntegrationTestSetup.kts @@ -11,7 +11,6 @@ import org.ostelco.prime.model.Segment import org.ostelco.prime.storage.graph.Neo4jStoreSingleton.atomicCreateOffer import org.ostelco.prime.storage.graph.Neo4jStoreSingleton.createIndex import org.ostelco.prime.storage.graph.createProduct -import org.ostelco.prime.storage.graph.getSegmentNameFromCountryCode private val logger by getLogger() @@ -52,7 +51,7 @@ job { atomicCreateOffer( offer = Offer(id = "default_offer"), - segments = listOf(Segment(id = getSegmentNameFromCountryCode("NO"))), + segments = listOf(Segment(id = "country-no")), products = listOf( createProduct(sku = "1GB_249NOK"), createProduct(sku = "2GB_299NOK"), diff --git a/neo4j-store/src/main/resources/OnNewCustomerAction.kts b/neo4j-store/src/main/resources/OnNewCustomerAction.kts index 2f3fda0a8..07bd21947 100644 --- a/neo4j-store/src/main/resources/OnNewCustomerAction.kts +++ b/neo4j-store/src/main/resources/OnNewCustomerAction.kts @@ -4,12 +4,13 @@ import arrow.effects.IO import arrow.instances.either.monad.monad import org.ostelco.prime.dsl.WriteTransaction import org.ostelco.prime.dsl.withSku +import org.ostelco.prime.model.Customer import org.ostelco.prime.model.Identity import org.ostelco.prime.model.Product import org.ostelco.prime.model.PurchaseRecord import org.ostelco.prime.storage.StoreError import org.ostelco.prime.storage.graph.Neo4jStoreSingleton.applyProduct -import org.ostelco.prime.storage.graph.Neo4jStoreSingleton.createPurchaseRecordRelation +import org.ostelco.prime.storage.graph.Neo4jStoreSingleton.createPurchaseRecord import org.ostelco.prime.storage.graph.OnNewCustomerAction import org.ostelco.prime.storage.graph.PrimeTransaction import java.time.Instant @@ -17,7 +18,7 @@ import java.util.* object : OnNewCustomerAction { override fun apply(identity: Identity, - customerId: String, + customer: Customer, transaction: PrimeTransaction): Either { val welcomePackProductSku = "2GB_FREE_ON_JOINING" @@ -26,8 +27,8 @@ object : OnNewCustomerAction { Either.monad().binding { WriteTransaction(transaction).apply { val product = get(Product withSku welcomePackProductSku).bind() - createPurchaseRecordRelation( - customerId, + createPurchaseRecord( + customer.id, PurchaseRecord( id = UUID.randomUUID().toString(), product = product, @@ -35,7 +36,7 @@ object : OnNewCustomerAction { ) ).bind() applyProduct( - customerId = customerId, + customerId = customer.id, product = product ).bind() } diff --git a/neo4j-store/src/main/resources/OnRegionApprovedAction.kts b/neo4j-store/src/main/resources/OnRegionApprovedAction.kts new file mode 100644 index 000000000..afac60c99 --- /dev/null +++ b/neo4j-store/src/main/resources/OnRegionApprovedAction.kts @@ -0,0 +1,37 @@ +import arrow.core.Either +import arrow.core.fix +import arrow.core.getOrElse +import arrow.effects.IO +import arrow.instances.either.monad.monad +import org.ostelco.prime.auditlog.AuditLog +import org.ostelco.prime.dsl.WriteTransaction +import org.ostelco.prime.dsl.withId +import org.ostelco.prime.model.Customer +import org.ostelco.prime.storage.StoreError +import org.ostelco.prime.storage.graph.OnRegionApprovedAction +import org.ostelco.prime.storage.graph.PrimeTransaction +import org.ostelco.prime.storage.graph.model.Segment + +object : OnRegionApprovedAction { + + override fun apply( + customer: Customer, + regionCode: String, + transaction: PrimeTransaction + ): Either { + return IO { + Either.monad().binding { + WriteTransaction(transaction).apply { + val segmentId = get(Segment withId "plan-country-${regionCode.toLowerCase()}") + .getOrElse { + get(Segment withId "country-${regionCode.toLowerCase()}").bind() + } + .id + fact { (Customer withId customer.id) belongsToSegment (Segment withId segmentId) }.bind() + AuditLog.info(customer.id, "Added customer to segment - $segmentId") + } + Unit + }.fix() + }.unsafeRunSync() + } +} \ No newline at end of file diff --git a/neo4j-store/src/test/kotlin/org/ostelco/prime/dsl/DSLTest.kt b/neo4j-store/src/test/kotlin/org/ostelco/prime/dsl/DSLTest.kt index dc8383806..1683d8c0c 100644 --- a/neo4j-store/src/test/kotlin/org/ostelco/prime/dsl/DSLTest.kt +++ b/neo4j-store/src/test/kotlin/org/ostelco/prime/dsl/DSLTest.kt @@ -81,17 +81,29 @@ class DSLTest { ConfigRegistry.config = Config( host = "0.0.0.0", protocol = "bolt", - hssNameLookupService = KtsServiceFactory( - serviceInterface = "org.ostelco.prime.storage.graph.HssNameLookupService", - textReader = ClasspathResourceTextReader( - filename = "/HssNameLookupService.kts" - ) - ), onNewCustomerAction = KtsServiceFactory( serviceInterface = "org.ostelco.prime.storage.graph.OnNewCustomerAction", textReader = ClasspathResourceTextReader( filename = "/OnNewCustomerAction.kts" ) + ), + allowedRegionsService = KtsServiceFactory( + serviceInterface = "org.ostelco.prime.storage.graph.AllowedRegionsService", + textReader = ClasspathResourceTextReader( + filename = "/AllowedRegionsService.kts" + ) + ), + onRegionApprovedAction = KtsServiceFactory( + serviceInterface = "org.ostelco.prime.storage.graph.OnRegionApprovedAction", + textReader = ClasspathResourceTextReader( + filename = "/OnRegionApprovedAction.kts" + ) + ), + hssNameLookupService = KtsServiceFactory( + serviceInterface = "org.ostelco.prime.storage.graph.HssNameLookupService", + textReader = ClasspathResourceTextReader( + filename = "/HssNameLookupService.kts" + ) ) ) Neo4jClient.start() diff --git a/neo4j-store/src/test/kotlin/org/ostelco/prime/storage/graph/Neo4jLoadTest.kt b/neo4j-store/src/test/kotlin/org/ostelco/prime/storage/graph/Neo4jLoadTest.kt index 707c20b29..c11747cb5 100644 --- a/neo4j-store/src/test/kotlin/org/ostelco/prime/storage/graph/Neo4jLoadTest.kt +++ b/neo4j-store/src/test/kotlin/org/ostelco/prime/storage/graph/Neo4jLoadTest.kt @@ -53,7 +53,7 @@ class Neo4jLoadTest { properties = mapOf(NO_OF_BYTES.s to "1_000_000_000")) } create { - Segment(id = getSegmentNameFromCountryCode(COUNTRY)) + Segment(id = "country-${COUNTRY.toLowerCase()}") } } } @@ -156,17 +156,29 @@ class Neo4jLoadTest { ConfigRegistry.config = Config( host = "0.0.0.0", protocol = "bolt", - hssNameLookupService = KtsServiceFactory( - serviceInterface = "org.ostelco.prime.storage.graph.HssNameLookupService", - textReader = ClasspathResourceTextReader( - filename = "/HssNameLookupService.kts" - ) - ), onNewCustomerAction = KtsServiceFactory( serviceInterface = "org.ostelco.prime.storage.graph.OnNewCustomerAction", textReader = ClasspathResourceTextReader( filename = "/OnNewCustomerAction.kts" ) + ), + allowedRegionsService = KtsServiceFactory( + serviceInterface = "org.ostelco.prime.storage.graph.AllowedRegionsService", + textReader = ClasspathResourceTextReader( + filename = "/AllowedRegionsService.kts" + ) + ), + onRegionApprovedAction = KtsServiceFactory( + serviceInterface = "org.ostelco.prime.storage.graph.OnRegionApprovedAction", + textReader = ClasspathResourceTextReader( + filename = "/OnRegionApprovedAction.kts" + ) + ), + hssNameLookupService = KtsServiceFactory( + serviceInterface = "org.ostelco.prime.storage.graph.HssNameLookupService", + textReader = ClasspathResourceTextReader( + filename = "/HssNameLookupService.kts" + ) ) ) Neo4jClient.start() diff --git a/neo4j-store/src/test/kotlin/org/ostelco/prime/storage/graph/Neo4jStoreTest.kt b/neo4j-store/src/test/kotlin/org/ostelco/prime/storage/graph/Neo4jStoreTest.kt index 29f5f8a2b..1861b2342 100644 --- a/neo4j-store/src/test/kotlin/org/ostelco/prime/storage/graph/Neo4jStoreTest.kt +++ b/neo4j-store/src/test/kotlin/org/ostelco/prime/storage/graph/Neo4jStoreTest.kt @@ -1,6 +1,10 @@ package org.ostelco.prime.storage.graph +import arrow.core.Either +import arrow.core.fix import arrow.core.right +import arrow.effects.IO +import arrow.instances.either.monad.monad import com.palantir.docker.compose.DockerComposeRule import com.palantir.docker.compose.connection.waiting.HealthChecks import kotlinx.coroutines.runBlocking @@ -14,10 +18,16 @@ import org.neo4j.driver.v1.AccessMode.WRITE import org.ostelco.prime.analytics.AnalyticsService import org.ostelco.prime.appnotifier.AppNotifier import org.ostelco.prime.dsl.DSL.job +import org.ostelco.prime.dsl.forExCustomer +import org.ostelco.prime.dsl.linkedToExCustomer +import org.ostelco.prime.dsl.readTransaction +import org.ostelco.prime.dsl.wasSubscribedBy +import org.ostelco.prime.dsl.withId import org.ostelco.prime.kts.engine.KtsServiceFactory import org.ostelco.prime.kts.engine.reader.ClasspathResourceTextReader import org.ostelco.prime.model.Customer import org.ostelco.prime.model.CustomerRegionStatus.APPROVED +import org.ostelco.prime.model.CustomerRegionStatus.AVAILABLE import org.ostelco.prime.model.CustomerRegionStatus.PENDING import org.ostelco.prime.model.Identity import org.ostelco.prime.model.JumioScanData @@ -41,6 +51,7 @@ import org.ostelco.prime.model.ScanStatus import org.ostelco.prime.model.SimEntry import org.ostelco.prime.model.SimProfile import org.ostelco.prime.model.SimProfileStatus.AVAILABLE_FOR_DOWNLOAD +import org.ostelco.prime.model.Subscription import org.ostelco.prime.notifications.EmailNotifier import org.ostelco.prime.paymentprocessor.PaymentProcessor import org.ostelco.prime.paymentprocessor.core.InvoiceInfo @@ -49,9 +60,12 @@ import org.ostelco.prime.paymentprocessor.core.ProfileInfo import org.ostelco.prime.sim.SimManager import org.ostelco.prime.storage.NotFoundError import org.ostelco.prime.storage.ScanInformationStore +import org.ostelco.prime.storage.StoreError +import org.ostelco.prime.storage.graph.model.ExCustomer import org.ostelco.prime.storage.graph.model.Segment import org.ostelco.prime.tracing.Trace import java.time.Instant +import java.time.LocalDate import java.util.* import javax.ws.rs.core.MultivaluedHashMap import javax.ws.rs.core.MultivaluedMap @@ -116,7 +130,7 @@ class Neo4jStoreTest { ) } create { - Segment(id = getSegmentNameFromCountryCode(REGION)) + Segment(id = "country-${REGION.toLowerCase()}") } } } @@ -149,9 +163,10 @@ class Neo4jStoreTest { referredBy = null) .mapLeft { fail(it.message) } - Neo4jStoreSingleton.getIdentityForContactEmail(contactEmail = EMAIL).bimap( + Neo4jStoreSingleton.getIdentitiesFor(queryString = EMAIL).bimap( { fail(it.message) }, - { identity: Identity -> + { list-> + val identity: Identity = list.first() Neo4jStoreSingleton.getCustomer(identity).bimap( { fail(it.message) }, { assertEquals(CUSTOMER, it) })}) @@ -166,9 +181,10 @@ class Neo4jStoreTest { referredBy = null) .mapLeft { fail(it.message) } - Neo4jStoreSingleton.getIdentityForContactEmail(contactEmail = EMAIL).bimap( + Neo4jStoreSingleton.getIdentitiesFor(queryString = EMAIL).bimap( { fail(it.message) }, - { identity: Identity -> + { list -> + val identity: Identity = list.first() assertEquals("EMAIL", identity.type) assertEquals(EMAIL, identity.id) assertEquals(IDENTITY.provider, identity.provider) @@ -231,10 +247,10 @@ class Neo4jStoreTest { val chargeId = UUID.randomUUID().toString() // mock - Mockito.`when`(mockPaymentProcessor.getPaymentProfile(customerId = CUSTOMER.id)) + `when`(mockPaymentProcessor.getPaymentProfile(customerId = CUSTOMER.id)) .thenReturn(ProfileInfo(EMAIL).right()) - Mockito.`when`(mockPaymentProcessor.createInvoice( + `when`(mockPaymentProcessor.createInvoice( customerId = CUSTOMER.id, amount = 24900, currency = "NOK", @@ -243,7 +259,7 @@ class Neo4jStoreTest { sourceId = null) ).thenReturn(InvoiceInfo(invoiceId).right()) - Mockito.`when`(mockPaymentProcessor.payInvoice( + `when`(mockPaymentProcessor.payInvoice( invoiceId = invoiceId) ).thenReturn(InvoicePaymentInfo(invoiceId, chargeId).right()) @@ -255,7 +271,7 @@ class Neo4jStoreTest { val offer = Offer( id = "NEW_OFFER", - segments = listOf(getSegmentNameFromCountryCode(REGION)), + segments = listOf("country-${REGION.toLowerCase()}"), products = listOf(sku)) Neo4jStoreSingleton.createOffer(offer) @@ -270,7 +286,7 @@ class Neo4jStoreTest { .mapLeft { fail(it.message) } Neo4jStoreSingleton.createCustomerRegionSetting( - customerId = CUSTOMER.id, + customer = CUSTOMER, status = APPROVED, regionCode = REGION_CODE) @@ -343,9 +359,10 @@ class Neo4jStoreTest { @Test fun `set and get Purchase record`() { - assert(Neo4jStoreSingleton.addCustomer( + Neo4jStoreSingleton.addCustomer( identity = IDENTITY, - customer = CUSTOMER).isRight()) + customer = CUSTOMER) + .mapLeft { fail(it.message) } val product = createProduct("1GB_249NOK") val now = Instant.now().toEpochMilli() @@ -370,9 +387,10 @@ class Neo4jStoreTest { @Test fun `create products, offer, segment and then get products for a customer`() { - assert(Neo4jStoreSingleton.addCustomer( + Neo4jStoreSingleton.addCustomer( identity = IDENTITY, - customer = CUSTOMER).isRight()) + customer = CUSTOMER) + .mapLeft { fail(it.message) } // prep job { @@ -474,9 +492,10 @@ class Neo4jStoreTest { create { Region(REGION_CODE, "Norway") } }.mapLeft { fail(it.message) } - assert(Neo4jStoreSingleton.addCustomer( + Neo4jStoreSingleton.addCustomer( identity = IDENTITY, - customer = CUSTOMER).isRight()) + customer = CUSTOMER) + .mapLeft { fail(it.message) } // test Neo4jStoreSingleton.createNewJumioKycScanId(identity = IDENTITY, regionCode = REGION_CODE).map { @@ -496,9 +515,10 @@ class Neo4jStoreTest { create { Region(REGION_CODE, "Norway") } }.mapLeft { fail(it.message) } - assert(Neo4jStoreSingleton.addCustomer( + Neo4jStoreSingleton.addCustomer( identity = IDENTITY, - customer = CUSTOMER).isRight()) + customer = CUSTOMER) + .mapLeft { fail(it.message) } // test Neo4jStoreSingleton.createNewJumioKycScanId(identity = IDENTITY, regionCode = REGION_CODE).map { newScan -> @@ -520,9 +540,10 @@ class Neo4jStoreTest { create { Region(REGION_CODE, "Norway") } }.mapLeft { fail(it.message) } - assert(Neo4jStoreSingleton.addCustomer( + Neo4jStoreSingleton.addCustomer( identity = IDENTITY, - customer = CUSTOMER).isRight()) + customer = CUSTOMER) + .mapLeft { fail(it.message) } Neo4jStoreSingleton.createNewJumioKycScanId(identity = IDENTITY, regionCode = REGION_CODE).map { val newScanInformation = ScanInformation( @@ -549,7 +570,7 @@ class Neo4jStoreTest { vendorData.add(JumioScanData.SCAN_IMAGE.s, imgUrl) vendorData.add(JumioScanData.SCAN_IMAGE_BACKSIDE.s, imgUrl2) - Mockito.`when`(mockScanInformationStore.upsertVendorScanInformation(customerId = CUSTOMER.id, countryCode = REGION, vendorData = vendorData)) + `when`(mockScanInformationStore.upsertVendorScanInformation(customerId = CUSTOMER.id, countryCode = REGION, vendorData = vendorData)) .thenReturn(Unit.right()) Neo4jStoreSingleton.updateScanInformation(newScanInformation, vendorData).mapLeft { @@ -568,9 +589,10 @@ class Neo4jStoreTest { create { Region(REGION_CODE, "Norway") } }.mapLeft { fail(it.message) } - assert(Neo4jStoreSingleton.addCustomer( + Neo4jStoreSingleton.addCustomer( identity = IDENTITY, - customer = CUSTOMER).isRight()) + customer = CUSTOMER) + .mapLeft { fail(it.message) } // test Neo4jStoreSingleton.createNewJumioKycScanId(identity = IDENTITY, regionCode = REGION_CODE).map { @@ -615,12 +637,14 @@ class Neo4jStoreTest { val fakeEmail = "fake-$EMAIL" val fakeIdentity = Identity(id = fakeEmail, type = "EMAIL", provider = "email") - assert(Neo4jStoreSingleton.addCustomer( + Neo4jStoreSingleton.addCustomer( identity = IDENTITY, - customer = CUSTOMER).isRight()) - assert(Neo4jStoreSingleton.addCustomer( + customer = CUSTOMER) + .mapLeft { fail(it.message) } + Neo4jStoreSingleton.addCustomer( identity = fakeIdentity, - customer = Customer(contactEmail = fakeEmail, nickname = NAME)).isRight()) + customer = Customer(contactEmail = fakeEmail, nickname = NAME)) + .mapLeft { fail(it.message) } // test Neo4jStoreSingleton.createNewJumioKycScanId(fakeIdentity, REGION_CODE).mapLeft { @@ -646,19 +670,21 @@ class Neo4jStoreTest { create { Region(REGION_CODE, "Norway") } }.mapLeft { fail(it.message) } - assert(Neo4jStoreSingleton.addCustomer( + Neo4jStoreSingleton.addCustomer( identity = IDENTITY, - customer = CUSTOMER).isRight()) + customer = CUSTOMER) + .mapLeft { fail(it.message) } - assert(Neo4jStoreSingleton.createCustomerRegionSetting( - customerId = CUSTOMER.id, + Neo4jStoreSingleton.createCustomerRegionSetting( + customer = CUSTOMER, status = APPROVED, - regionCode = REGION_CODE).isRight()) + regionCode = REGION_CODE) + .mapLeft { fail(it.message) } - Mockito.`when`(mockSimManager.allocateNextEsimProfile("Loltel", "default")) + `when`(mockSimManager.allocateNextEsimProfile("Loltel", "default")) .thenReturn(SimEntry(iccId = "iccId", eSimActivationCode = "eSimActivationCode", msisdnList = emptyList(), status = AVAILABLE_FOR_DOWNLOAD).right()) - Mockito.`when`(mockSimManager.getSimProfile("Loltel", "iccId")) + `when`(mockSimManager.getSimProfile("Loltel", "iccId")) .thenReturn(SimEntry(iccId = "iccId", eSimActivationCode = "eSimActivationCode", msisdnList = emptyList(), status = AVAILABLE_FOR_DOWNLOAD).right()) // test @@ -699,15 +725,20 @@ class Neo4jStoreTest { create { Region(REGION_CODE, "Norway") } }.mapLeft { fail(it.message) } - assert(Neo4jStoreSingleton.addCustomer( + Neo4jStoreSingleton.addCustomer( identity = IDENTITY, - customer = CUSTOMER).isRight()) + customer = CUSTOMER) + .mapLeft { fail(it.message) } // test Neo4jStoreSingleton.getAllRegionDetails(identity = IDENTITY) .bimap( - { fail("Failed to fetch regions empty list") }, - { assert(it.isEmpty()) { "Regions list should be empty" } }) + { fail("Failed to fetch regions list") }, + { + for (region in it) { + assert(region.status == AVAILABLE) { "All regions should be marked available" } + } + }) } @Test @@ -717,9 +748,10 @@ class Neo4jStoreTest { create { Region(REGION_CODE, "Norway") } }.mapLeft { fail(it.message) } - assert(Neo4jStoreSingleton.addCustomer( + Neo4jStoreSingleton.addCustomer( identity = IDENTITY, - customer = CUSTOMER).isRight()) + customer = CUSTOMER) + .mapLeft { fail(it.message) } // test Neo4jStoreSingleton.getRegionDetails(identity = IDENTITY, regionCode = REGION_CODE) @@ -740,18 +772,21 @@ class Neo4jStoreTest { create { Region("sg", "Singapore") } }.mapLeft { fail(it.message) } - assert(Neo4jStoreSingleton.addCustomer( + Neo4jStoreSingleton.addCustomer( identity = IDENTITY, - customer = CUSTOMER).isRight()) + customer = CUSTOMER) + .mapLeft { fail(it.message) } - assert(Neo4jStoreSingleton.createCustomerRegionSetting( - customerId = CUSTOMER.id, + Neo4jStoreSingleton.createCustomerRegionSetting( + customer = CUSTOMER, status = APPROVED, - regionCode = "no").isRight()) - assert(Neo4jStoreSingleton.createCustomerRegionSetting( - customerId = CUSTOMER.id, + regionCode = "no") + .mapLeft { fail(it.message) } + Neo4jStoreSingleton.createCustomerRegionSetting( + customer = CUSTOMER, status = PENDING, - regionCode = "sg").isRight()) + regionCode = "sg") + .mapLeft { fail(it.message) } // test Neo4jStoreSingleton.getAllRegionDetails(identity = IDENTITY) @@ -784,18 +819,22 @@ class Neo4jStoreTest { create { Region("sg", "Singapore") } }.mapLeft { fail(it.message) } - assert(Neo4jStoreSingleton.addCustomer( + Neo4jStoreSingleton.addCustomer( identity = IDENTITY, - customer = CUSTOMER).isRight()) + customer = CUSTOMER) + .mapLeft { fail(it.message) } - assert(Neo4jStoreSingleton.createCustomerRegionSetting( - customerId = CUSTOMER.id, + Neo4jStoreSingleton.createCustomerRegionSetting( + customer = CUSTOMER, status = APPROVED, - regionCode = "no").isRight()) - assert(Neo4jStoreSingleton.createCustomerRegionSetting( - customerId = CUSTOMER.id, + regionCode = "no") + .mapLeft { fail(it.message) } + + Neo4jStoreSingleton.createCustomerRegionSetting( + customer = CUSTOMER, status = PENDING, - regionCode = "sg").isRight()) + regionCode = "sg") + .mapLeft { fail(it.message) } // test Neo4jStoreSingleton.getRegionDetails(identity = IDENTITY, regionCode = REGION_CODE) @@ -823,29 +862,34 @@ class Neo4jStoreTest { create { Region("sg", "Singapore") } }.mapLeft { fail(it.message) } - assert(Neo4jStoreSingleton.addCustomer( + Neo4jStoreSingleton.addCustomer( identity = IDENTITY, - customer = CUSTOMER).isRight()) + customer = CUSTOMER) + .mapLeft { fail(it.message) } - assert(Neo4jStoreSingleton.createCustomerRegionSetting( - customerId = CUSTOMER.id, + Neo4jStoreSingleton.createCustomerRegionSetting( + customer = CUSTOMER, status = APPROVED, - regionCode = "no").isRight()) - assert(Neo4jStoreSingleton.createCustomerRegionSetting( - customerId = CUSTOMER.id, + regionCode = "no") + .mapLeft { fail(it.message) } + + Neo4jStoreSingleton.createCustomerRegionSetting( + customer = CUSTOMER, status = PENDING, - regionCode = "sg").isRight()) + regionCode = "sg") + .mapLeft { fail(it.message) } - Mockito.`when`(mockSimManager.allocateNextEsimProfile("Loltel", "default")) + `when`(mockSimManager.allocateNextEsimProfile("Loltel", "default")) .thenReturn(SimEntry(iccId = "iccId", eSimActivationCode = "eSimActivationCode", msisdnList = emptyList(), status = AVAILABLE_FOR_DOWNLOAD).right()) - Mockito.`when`(mockSimManager.getSimProfile("Loltel", "iccId")) + `when`(mockSimManager.getSimProfile("Loltel", "iccId")) .thenReturn(SimEntry(iccId = "iccId", eSimActivationCode = "eSimActivationCode", msisdnList = emptyList(), status = AVAILABLE_FOR_DOWNLOAD).right()) - assert(Neo4jStoreSingleton.provisionSimProfile( + Neo4jStoreSingleton.provisionSimProfile( identity = IDENTITY, regionCode = REGION_CODE, - profileType = "default").isRight()) + profileType = "default") + .mapLeft { fail(it.message) } // test Neo4jStoreSingleton.getAllRegionDetails(identity = IDENTITY) @@ -887,29 +931,33 @@ class Neo4jStoreTest { create { Region("sg", "Singapore") } }.mapLeft { fail(it.message) } - assert(Neo4jStoreSingleton.addCustomer( + Neo4jStoreSingleton.addCustomer( identity = IDENTITY, - customer = CUSTOMER).isRight()) + customer = CUSTOMER) + .mapLeft { fail(it.message) } - assert(Neo4jStoreSingleton.createCustomerRegionSetting( - customerId = CUSTOMER.id, + Neo4jStoreSingleton.createCustomerRegionSetting( + customer = CUSTOMER, status = APPROVED, - regionCode = "no").isRight()) - assert(Neo4jStoreSingleton.createCustomerRegionSetting( - customerId = CUSTOMER.id, + regionCode = "no") + .mapLeft { fail(it.message) } + Neo4jStoreSingleton.createCustomerRegionSetting( + customer = CUSTOMER, status = PENDING, - regionCode = "sg").isRight()) + regionCode = "sg") + .mapLeft { fail(it.message) } - Mockito.`when`(mockSimManager.allocateNextEsimProfile("Loltel", "default")) + `when`(mockSimManager.allocateNextEsimProfile("Loltel", "default")) .thenReturn(SimEntry(iccId = "iccId", eSimActivationCode = "eSimActivationCode", msisdnList = emptyList(), status = AVAILABLE_FOR_DOWNLOAD).right()) - Mockito.`when`(mockSimManager.getSimProfile("Loltel", "iccId")) + `when`(mockSimManager.getSimProfile("Loltel", "iccId")) .thenReturn(SimEntry(iccId = "iccId", eSimActivationCode = "eSimActivationCode", msisdnList = emptyList(), status = AVAILABLE_FOR_DOWNLOAD).right()) - assert(Neo4jStoreSingleton.provisionSimProfile( + Neo4jStoreSingleton.provisionSimProfile( identity = IDENTITY, regionCode = REGION_CODE, - profileType = "default").isRight()) + profileType = "default") + .mapLeft { fail(it.message) } // test Neo4jStoreSingleton.getRegionDetails(identity = IDENTITY, regionCode = REGION_CODE) @@ -937,13 +985,13 @@ class Neo4jStoreTest { create { Region("sg", "Singapore") } }.mapLeft { fail(it.message) } - /* Note: (kmm) For 'sg' the first segment offered is always a plan. */ - Neo4jStoreSingleton.createSegment(org.ostelco.prime.model.Segment(id = getPlanSegmentNameFromCountryCode("sg"))) + Neo4jStoreSingleton.createSegment(org.ostelco.prime.model.Segment(id = "country-sg")) .mapLeft { fail(it.message) } - assert(Neo4jStoreSingleton.addCustomer( + Neo4jStoreSingleton.addCustomer( identity = IDENTITY, - customer = CUSTOMER).isRight()) + customer = CUSTOMER) + .mapLeft { fail(it.message) } Neo4jStoreSingleton.getRegionDetails( identity = IDENTITY, @@ -953,7 +1001,7 @@ class Neo4jStoreTest { } Neo4jStoreSingleton.setKycStatus( - customerId = CUSTOMER.id, + customer = CUSTOMER, regionCode = "sg", kycType = MY_INFO) .mapLeft { fail(it.message) } @@ -968,7 +1016,7 @@ class Neo4jStoreTest { }) Neo4jStoreSingleton.setKycStatus( - customerId = CUSTOMER.id, + customer = CUSTOMER, regionCode = "sg", kycType = ADDRESS) .mapLeft { fail(it.message) } @@ -986,16 +1034,16 @@ class Neo4jStoreTest { @Test fun `test NRIC_FIN JUMIO and ADDRESS_PHONE status`() { - /* Note: (kmm) For 'sg' the first segment offered is always a plan. */ - Neo4jStoreSingleton.createSegment(org.ostelco.prime.model.Segment(id = getPlanSegmentNameFromCountryCode("sg"))) + Neo4jStoreSingleton.createSegment(org.ostelco.prime.model.Segment(id = "country-sg")) job { create { Region("sg", "Singapore") } }.mapLeft { fail(it.message) } - assert(Neo4jStoreSingleton.addCustomer( + Neo4jStoreSingleton.addCustomer( identity = IDENTITY, - customer = CUSTOMER).isRight()) + customer = CUSTOMER) + .mapLeft { fail(it.message) } Neo4jStoreSingleton.getRegionDetails( identity = IDENTITY, @@ -1005,9 +1053,12 @@ class Neo4jStoreTest { } Neo4jStoreSingleton.setKycStatus( - customerId = CUSTOMER.id, + customer = CUSTOMER, regionCode = "sg", kycType = NRIC_FIN) + .mapLeft { + fail(it.message) + } Neo4jStoreSingleton.getRegionDetails( identity = IDENTITY, @@ -1019,9 +1070,12 @@ class Neo4jStoreTest { }) Neo4jStoreSingleton.setKycStatus( - customerId = CUSTOMER.id, + customer = CUSTOMER, regionCode = "sg", kycType = JUMIO) + .mapLeft { + fail(it.message) + } Neo4jStoreSingleton.getRegionDetails( identity = IDENTITY, @@ -1033,9 +1087,12 @@ class Neo4jStoreTest { }) Neo4jStoreSingleton.setKycStatus( - customerId = CUSTOMER.id, + customer = CUSTOMER, regionCode = "sg", kycType = ADDRESS) + .mapLeft { + fail(it.message) + } Neo4jStoreSingleton.getRegionDetails( identity = IDENTITY, @@ -1047,6 +1104,86 @@ class Neo4jStoreTest { }) } + @Test + fun `test delete customer`() { + + // setup + job { + create { Region("sg", "Singapore") } + create { Segment(id = "country-sg") } + }.mapLeft { fail(it.message) } + + IO { + Either.monad().binding { + + Neo4jStoreSingleton.addCustomer( + identity = IDENTITY, + customer = CUSTOMER).bind() + + Neo4jStoreSingleton.approveRegionForCustomer( + customerId = CUSTOMER.id, + regionCode = "sg") + .bind() + + Neo4jStoreSingleton.addSubscription( + identity = IDENTITY, + regionCode = "sg", + iccId = ICC_ID, + alias = ALIAS, + msisdn = MSISDN) + .mapLeft { fail(it.message) } + .bind() + + // test + Neo4jStoreSingleton.removeCustomer(identity = IDENTITY).bind() + }.fix() + }.unsafeRunSync() + .mapLeft { fail(it.message) } + + // asserts + readTransaction { + IO { + Either.monad().binding { + + val exCustomer = get(ExCustomer withId CUSTOMER.id).bind() + assertEquals( + expected = ExCustomer(id = CUSTOMER.id, terminationDate = "%d-%02d-%02d".format(LocalDate.now().year, LocalDate.now().monthValue, LocalDate.now().dayOfMonth)), + actual = exCustomer, + message = "ExCustomer does not match") + + val simProfiles = get(org.ostelco.prime.storage.graph.model.SimProfile forExCustomer (ExCustomer withId CUSTOMER.id)).bind() + assertEquals(expected = 1, actual = simProfiles.size, message = "No SIM profiles found for ExCustomer") + + val simProfile = simProfiles[0] + assertEquals( + expected = simProfile.iccId, + actual = ICC_ID, + message = "ICC ID of simProfile for ExCustomer do not match") + + val subscriptions = get(Subscription wasSubscribedBy (ExCustomer withId CUSTOMER.id)).bind() + assertEquals(expected = 1, actual = subscriptions.size, message = "No subscriptions found for ExCustomer") + + val subscription = subscriptions[0] + assertEquals( + expected = subscription.msisdn, + actual = MSISDN, + message = "MSISDN of subscription for ExCustomer do not match") + + val regions = get(Region linkedToExCustomer (ExCustomer withId CUSTOMER.id)).bind() + assertEquals(expected = 1, actual = regions.size, message = "No regions found for ExCustomer") + + val region = regions[0] + assertEquals( + expected = Region("sg", "Singapore"), + actual = region, + message = "Region for ExCustomer do not match") + + }.fix() + }.unsafeRunSync() + .mapLeft { fail(it.message) } + } + } + companion object { const val EMAIL = "foo@bar.com" const val NAME = "Test User" @@ -1076,17 +1213,29 @@ class Neo4jStoreTest { ConfigRegistry.config = Config( host = "0.0.0.0", protocol = "bolt", - hssNameLookupService = KtsServiceFactory( - serviceInterface = "org.ostelco.prime.storage.graph.HssNameLookupService", - textReader = ClasspathResourceTextReader( - filename = "/HssNameLookupService.kts" - ) - ), onNewCustomerAction = KtsServiceFactory( serviceInterface = "org.ostelco.prime.storage.graph.OnNewCustomerAction", textReader = ClasspathResourceTextReader( filename = "/OnNewCustomerAction.kts" ) + ), + allowedRegionsService = KtsServiceFactory( + serviceInterface = "org.ostelco.prime.storage.graph.AllowedRegionsService", + textReader = ClasspathResourceTextReader( + filename = "/AllowedRegionsService.kts" + ) + ), + onRegionApprovedAction = KtsServiceFactory( + serviceInterface = "org.ostelco.prime.storage.graph.OnRegionApprovedAction", + textReader = ClasspathResourceTextReader( + filename = "/OnRegionApprovedAction.kts" + ) + ), + hssNameLookupService = KtsServiceFactory( + serviceInterface = "org.ostelco.prime.storage.graph.HssNameLookupService", + textReader = ClasspathResourceTextReader( + filename = "/HssNameLookupService.kts" + ) ) ) Neo4jClient.start() @@ -1098,4 +1247,4 @@ class Neo4jStoreTest { Neo4jClient.stop() } } -} \ No newline at end of file +} diff --git a/neo4j-store/src/test/kotlin/org/ostelco/prime/storage/graph/SchemaTest.kt b/neo4j-store/src/test/kotlin/org/ostelco/prime/storage/graph/SchemaTest.kt index cd9a0fd04..1e3530ba0 100644 --- a/neo4j-store/src/test/kotlin/org/ostelco/prime/storage/graph/SchemaTest.kt +++ b/neo4j-store/src/test/kotlin/org/ostelco/prime/storage/graph/SchemaTest.kt @@ -191,7 +191,7 @@ class SchemaTest { @Test fun `json to map`() { - val map = objectMapper.readValue>("""{"label":"3GB for 300 NOK"}""", object : TypeReference>() {}) + val map = objectMapper.readValue>("""{"label":"3GB for 300 NOK"}""", object : TypeReference>() {}) assertEquals("3GB for 300 NOK", map["label"]) } @@ -258,17 +258,29 @@ class SchemaTest { ConfigRegistry.config = Config( host = "0.0.0.0", protocol = "bolt", - hssNameLookupService = KtsServiceFactory( - serviceInterface = "org.ostelco.prime.storage.graph.HssNameLookupService", - textReader = ClasspathResourceTextReader( - filename = "/HssNameLookupService.kts" - ) - ), onNewCustomerAction = KtsServiceFactory( serviceInterface = "org.ostelco.prime.storage.graph.OnNewCustomerAction", textReader = ClasspathResourceTextReader( filename = "/OnNewCustomerAction.kts" ) + ), + allowedRegionsService = KtsServiceFactory( + serviceInterface = "org.ostelco.prime.storage.graph.AllowedRegionsService", + textReader = ClasspathResourceTextReader( + filename = "/AllowedRegionsService.kts" + ) + ), + onRegionApprovedAction = KtsServiceFactory( + serviceInterface = "org.ostelco.prime.storage.graph.OnRegionApprovedAction", + textReader = ClasspathResourceTextReader( + filename = "/OnRegionApprovedAction.kts" + ) + ), + hssNameLookupService = KtsServiceFactory( + serviceInterface = "org.ostelco.prime.storage.graph.HssNameLookupService", + textReader = ClasspathResourceTextReader( + filename = "/HssNameLookupService.kts" + ) ) ) Neo4jClient.start() diff --git a/ocs-ktc/build.gradle.kts b/ocs-ktc/build.gradle.kts index a2fec0b9b..4dd5cdcc6 100644 --- a/ocs-ktc/build.gradle.kts +++ b/ocs-ktc/build.gradle.kts @@ -7,6 +7,7 @@ plugins { dependencies { implementation(project(":prime-modules")) + api(project(":kts-engine")) implementation("com.google.cloud:google-cloud-pubsub:${Version.googleCloudPubSub}") @@ -19,4 +20,4 @@ dependencies { } -apply(from = "../gradle/jacoco.gradle") \ No newline at end of file +apply(from = "../gradle/jacoco.gradle.kts") \ 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 index c1f750b4b..bd8051836 100644 --- a/ocs-ktc/src/main/kotlin/org/ostelco/prime/ocs/OcsModule.kt +++ b/ocs-ktc/src/main/kotlin/org/ostelco/prime/ocs/OcsModule.kt @@ -3,6 +3,7 @@ package org.ostelco.prime.ocs import com.fasterxml.jackson.annotation.JsonProperty import com.fasterxml.jackson.annotation.JsonTypeName import io.dropwizard.setup.Environment +import org.ostelco.prime.kts.engine.KtsServiceFactory import org.ostelco.prime.module.PrimeModule import org.ostelco.prime.ocs.ConfigRegistry.config import org.ostelco.prime.ocs.activation.ActivateEventObservableSingleton @@ -10,7 +11,6 @@ import org.ostelco.prime.ocs.consumption.grpc.OcsGrpcServer import org.ostelco.prime.ocs.consumption.grpc.OcsGrpcService import org.ostelco.prime.ocs.consumption.pubsub.PubSubClient import org.ostelco.prime.ocs.core.OnlineCharging -import org.ostelco.prime.ocs.core.Rating @JsonTypeName("ocs") class OcsModule : PrimeModule { @@ -43,10 +43,6 @@ class OcsModule : PrimeModule { } ) } - - config.rating?.forEach { rate -> - Rating.addRate(rate.serviceId, rate.ratingGroup, rate.rate) - } } } @@ -63,7 +59,7 @@ data class PubSubChannel( data class Config( val lowBalanceThreshold: Long = 0, val pubSubChannel: PubSubChannel? = null, - val rating: ArrayList? = null) + val consumptionPolicyService: KtsServiceFactory) object ConfigRegistry { lateinit var config: Config diff --git a/ocs-ktc/src/main/kotlin/org/ostelco/prime/ocs/consumption/pubsub/PubSubClient.kt b/ocs-ktc/src/main/kotlin/org/ostelco/prime/ocs/consumption/pubsub/PubSubClient.kt index 5b6d83d04..1779c4c6c 100644 --- a/ocs-ktc/src/main/kotlin/org/ostelco/prime/ocs/consumption/pubsub/PubSubClient.kt +++ b/ocs-ktc/src/main/kotlin/org/ostelco/prime/ocs/consumption/pubsub/PubSubClient.kt @@ -101,10 +101,13 @@ class PubSubClient( override fun onFailure(throwable: Throwable) { if (throwable is ApiException) { // details on the API exception - logger.error("Status code: {}", throwable.statusCode.code) - logger.error("Retrying: {}", throwable.isRetryable) + logger.warn("Pubsub messageId: $messageId\n" + + "Message : ${throwable.message}\n" + + "Status code: ${throwable.statusCode.code}\n" + + "Retrying: ${throwable.isRetryable}") + } else { + logger.error("Error sending CCR Request to PubSub. messageId: $messageId") } - logger.error("Error sending CCR Request to PubSub") } override fun onSuccess(messageId: String) { diff --git a/ocs-ktc/src/main/kotlin/org/ostelco/prime/ocs/core/KtsServices.kt b/ocs-ktc/src/main/kotlin/org/ostelco/prime/ocs/core/KtsServices.kt new file mode 100644 index 000000000..7f05eaa74 --- /dev/null +++ b/ocs-ktc/src/main/kotlin/org/ostelco/prime/ocs/core/KtsServices.kt @@ -0,0 +1,26 @@ +package org.ostelco.prime.ocs.core + +import arrow.core.Either +import org.ostelco.ocs.api.MultipleServiceCreditControl +import org.ostelco.prime.storage.ConsumptionResult + +data class ConsumptionRequest( + val msisdn: String, + val usedBytes: Long, + val requestedBytes: Long +) + +interface ConsumptionPolicy { + + /** + * This function will either return [ConsumptionResult] as [Either]::Left, which is then to be returned back to PGw. + * Or it will return Consumption Request as [Either]::Right, which is then to be passed to Storage for persistence. + * And then the result from Storage will be [ConsumptionResult], which will be returned back to PGw. + */ + fun checkConsumption( + msisdn: String, + multipleServiceCreditControl: MultipleServiceCreditControl, + mccMnc: String, + apn: String + ): Either +} 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 index 834e3d089..8baceca2a 100644 --- 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 @@ -1,7 +1,5 @@ package org.ostelco.prime.ocs.core -import arrow.core.Either -import arrow.core.right import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch @@ -14,12 +12,12 @@ import org.ostelco.ocs.api.ResultCode import org.ostelco.ocs.api.ServiceUnit import org.ostelco.prime.getLogger import org.ostelco.prime.module.getResource +import org.ostelco.prime.ocs.ConfigRegistry 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.AdminDataSource import org.ostelco.prime.storage.ConsumptionResult -import org.ostelco.prime.storage.StoreError import java.util.concurrent.CountDownLatch import java.util.concurrent.TimeUnit @@ -27,12 +25,16 @@ import java.util.concurrent.TimeUnit object OnlineCharging : OcsAsyncRequestConsumer { var loadUnitTest = false + val keepAliveMsisdn = "keepalive" private val loadAcceptanceTest = System.getenv("LOAD_TESTING") == "true" - private val storage: AdminDataSource = getResource() - private val logger by getLogger() + private val storage: AdminDataSource = getResource() + private val consumptionPolicy by lazy { + ConfigRegistry.config.consumptionPolicyService.getKtsService() + } + override fun creditControlRequestEvent( request: CreditControlRequestInfo, returnCreditControlAnswer: (CreditControlAnswerInfo) -> Unit) { @@ -40,63 +42,110 @@ object OnlineCharging : OcsAsyncRequestConsumer { val msisdn = request.msisdn if (msisdn != null) { + if (isKeepAlive(request)) { + handleKeepAlive(request, returnCreditControlAnswer) + } else { + chargeRequest(request, msisdn, returnCreditControlAnswer) + } + } + } - val responseBuilder = CreditControlAnswerInfo.newBuilder() - responseBuilder.setRequestNumber(request.requestNumber) + private fun isKeepAlive(request: CreditControlRequestInfo): Boolean = request.msisdn == keepAliveMsisdn + + private fun handleKeepAlive(request: CreditControlRequestInfo, returnCreditControlAnswer: (CreditControlAnswerInfo) -> Unit) { + val responseBuilder = CreditControlAnswerInfo.newBuilder() + .setRequestNumber(request.requestNumber) + .setRequestId(request.requestId) + .setMsisdn(keepAliveMsisdn) + .setResultCode(ResultCode.UNKNOWN) + + returnCreditControlAnswer(responseBuilder.buildPartial()) + } + + private fun chargeRequest(request: CreditControlRequestInfo, + msisdn: String, + returnCreditControlAnswer: (CreditControlAnswerInfo) -> Unit) { - // these are keepalives to keep latency low - if (msisdn == "keepalive") { - responseBuilder.setRequestId(request.requestId).setMsisdn("keepalive").resultCode = ResultCode.UNKNOWN - returnCreditControlAnswer(responseBuilder.buildPartial()) + CoroutineScope(Dispatchers.Default).launch { + + val responseBuilder = CreditControlAnswerInfo.newBuilder() + responseBuilder.requestNumber = request.requestNumber + responseBuilder.setRequestId(request.requestId) + .setMsisdn(msisdn) + .resultCode = ResultCode.DIAMETER_SUCCESS + + if (request.msccCount == 0) { + responseBuilder.validityTime = 86400 + storage.consume(msisdn, 0L, 0L) { storeResult -> + storeResult.fold( + { responseBuilder.resultCode = ResultCode.DIAMETER_USER_UNKNOWN }, + { responseBuilder.resultCode = ResultCode.DIAMETER_SUCCESS }) + } } else { + chargeMSCCs(request, msisdn, responseBuilder) + } - CoroutineScope(Dispatchers.Default).launch { - - responseBuilder.setRequestId(request.requestId) - .setMsisdn(msisdn).setResultCode(ResultCode.DIAMETER_SUCCESS) - - val doneSignal = CountDownLatch(request.msccList.size) - - request.msccList.forEach { mscc -> - - val requested = mscc.requested?.totalOctets ?: 0 - if (requested > 0) { - charge(msisdn, mscc, request.serviceInformation.psInformation.sgsnMccMnc) { storeResult -> - storeResult.fold( - { - // FixMe - responseBuilder.resultCode = ResultCode.DIAMETER_USER_UNKNOWN - doneSignal.countDown() - }, - { consumptionResult -> - addGrantedQuota(consumptionResult.granted, mscc, responseBuilder) - addInfo(consumptionResult.balance, mscc, responseBuilder) - reportAnalytics(consumptionResult, request) - Notifications.lowBalanceAlert(msisdn, consumptionResult.granted, consumptionResult.balance) - doneSignal.countDown() - } - ) - } - } else { - doneSignal.countDown() - } - } - doneSignal.await(2, TimeUnit.SECONDS) - - if (responseBuilder.msccCount == 0) { - responseBuilder.validityTime = 86400 - } - - synchronized(OnlineCharging) { - returnCreditControlAnswer(responseBuilder.build()) - } + synchronized(OnlineCharging) { + returnCreditControlAnswer(responseBuilder.build()) + } + } + } + + private suspend fun chargeMSCCs(request: CreditControlRequestInfo, + msisdn: String, + responseBuilder: CreditControlAnswerInfo.Builder) { + + val doneSignal = CountDownLatch(request.msccList.size) + + request.msccList.forEach { mscc -> + + fun consumptionResultHandler(consumptionResult: ConsumptionResult) { + addGrantedQuota(consumptionResult.granted, mscc, responseBuilder) + addInfo(consumptionResult.balance, mscc, responseBuilder) + reportAnalytics(consumptionResult, request) + Notifications.lowBalanceAlert(msisdn, consumptionResult.granted, consumptionResult.balance) + doneSignal.countDown() + } + + suspend fun consumeRequestHandler(consumptionRequest: ConsumptionRequest) { + storage.consume( + msisdn = consumptionRequest.msisdn, + usedBytes = consumptionRequest.usedBytes, + requestedBytes = consumptionRequest.requestedBytes) { storeResult -> + + storeResult + .fold( + { storeError -> + // FixMe : should all store errors be unknown user? + logger.error(storeError.message) + responseBuilder.resultCode = ResultCode.DIAMETER_USER_UNKNOWN + doneSignal.countDown() + }, + { consumptionResult -> consumptionResultHandler(consumptionResult) } + ) } } + + val requested = mscc.requested?.totalOctets ?: 0 + if (requested > 0) { + consumptionPolicy.checkConsumption( + msisdn = msisdn, + multipleServiceCreditControl = mscc, + mccMnc = request.serviceInformation.psInformation.sgsnMccMnc, + apn = request.serviceInformation.psInformation.calledStationId) + .bimap( + { consumptionResult -> consumptionResultHandler(consumptionResult) }, + { consumptionRequest -> consumeRequestHandler(consumptionRequest) } + ) + } else { + doneSignal.countDown() + } } + doneSignal.await(2, TimeUnit.SECONDS) } - private fun reportAnalytics(consumptionResult : ConsumptionResult, request: CreditControlRequestInfo) { + private fun reportAnalytics(consumptionResult: ConsumptionResult, request: CreditControlRequestInfo) { if (!loadUnitTest && !loadAcceptanceTest) { CoroutineScope(Dispatchers.Default).launch { AnalyticsReporter.report( @@ -154,16 +203,4 @@ object OnlineCharging : OcsAsyncRequestConsumer { response.addMscc(responseMscc.build()) } } - - private suspend fun charge(msisdn: String, multipleServiceCreditControl: MultipleServiceCreditControl, mccmnc: String, callback: (Either) -> Unit) { - - val requested = multipleServiceCreditControl.requested?.totalOctets ?: 0 - val used = multipleServiceCreditControl.used?.totalOctets ?: 0 - - when (Rating.getRate(msisdn, multipleServiceCreditControl.serviceIdentifier, multipleServiceCreditControl.ratingGroup, mccmnc)) { - Rating.Rate.ZERO -> callback(ConsumptionResult(msisdn, multipleServiceCreditControl.requested.totalOctets, multipleServiceCreditControl.requested.totalOctets * 100).right()) - Rating.Rate.NORMAL -> storage.consume(msisdn, used, requested, callback) - Rating.Rate.BLOCKED -> callback(ConsumptionResult(msisdn, 0L, 0L).right()) - } - } } \ No newline at end of file diff --git a/ocs-ktc/src/main/kotlin/org/ostelco/prime/ocs/core/Rating.kt b/ocs-ktc/src/main/kotlin/org/ostelco/prime/ocs/core/Rating.kt deleted file mode 100644 index 8ee87875e..000000000 --- a/ocs-ktc/src/main/kotlin/org/ostelco/prime/ocs/core/Rating.kt +++ /dev/null @@ -1,30 +0,0 @@ -package org.ostelco.prime.ocs.core - -import org.ostelco.prime.getLogger - -object Rating { - enum class Rate { - BLOCKED, NORMAL, ZERO - } - - private val logger by getLogger() - private val hashMap: HashMap = HashMap() - - /** - * The rate should be set based on the subscription, location, Service-Identifier - * and the Rating-Group. - */ - @Suppress("UNUSED_PARAMETER") - fun getRate(msisdn: String, serviceIdentifier: Long, ratingGroup: Long, mccmnc: String) : Rate { - return hashMap.get(RateIdentifier(serviceIdentifier, ratingGroup)) ?: Rating.Rate.BLOCKED - } - - fun addRate(serviceIdentifier: Long, ratingGroup: Long, rate: String) { - logger.info("Adding rate for {} {} : {}", serviceIdentifier, ratingGroup, rate) - hashMap.put(RateIdentifier(serviceIdentifier, ratingGroup), Rating.Rate.valueOf(rate.toUpperCase())) - } - - data class RateIdentifier( - val serviceId: Long, - val ratingGroup: Long) -} \ No newline at end of file diff --git a/ocs-ktc/src/main/resources/.gitignore b/ocs-ktc/src/main/resources/.gitignore new file mode 100644 index 000000000..93461aabb --- /dev/null +++ b/ocs-ktc/src/main/resources/.gitignore @@ -0,0 +1 @@ +ocs-kts \ No newline at end of file diff --git a/ocs-ktc/src/main/resources/ConsumptionPolicyService.kts b/ocs-ktc/src/main/resources/ConsumptionPolicyService.kts new file mode 100644 index 000000000..b0501d464 --- /dev/null +++ b/ocs-ktc/src/main/resources/ConsumptionPolicyService.kts @@ -0,0 +1,51 @@ +import arrow.core.Either +import arrow.core.left +import arrow.core.right +import org.ostelco.ocs.api.MultipleServiceCreditControl +import org.ostelco.prime.ocs.core.ConsumptionPolicy +import org.ostelco.prime.ocs.core.ConsumptionRequest +import org.ostelco.prime.storage.ConsumptionResult + +private data class ServiceIdRatingGroup( + val serviceId: Long, + val ratingGroup: Long +) + +object : ConsumptionPolicy { + + override fun checkConsumption( + msisdn: String, + multipleServiceCreditControl: MultipleServiceCreditControl, + mccMnc: String, + apn: String): Either { + + val requested = multipleServiceCreditControl.requested?.totalOctets ?: 0 + val used = multipleServiceCreditControl.used?.totalOctets ?: 0 + + return when (ServiceIdRatingGroup( + serviceId = multipleServiceCreditControl.serviceIdentifier, + ratingGroup = multipleServiceCreditControl.ratingGroup)) { + + // NORMAL + ServiceIdRatingGroup(1L, 10L), // Test + ServiceIdRatingGroup(2L, 12L), // Test + ServiceIdRatingGroup(4L, 14L), // Test + ServiceIdRatingGroup(-1L, 10L) /* Test */ -> { + ConsumptionRequest( + msisdn = msisdn, + usedBytes = used, + requestedBytes = requested + ).right() + } + + // BLOCKED + else -> { + ConsumptionResult( + msisdnAnalyticsId = msisdn, + granted = 0L, + balance = 0L + ).left() + } + } + } +} diff --git a/ocsgw/Dockerfile b/ocsgw/Dockerfile index 78c5471b3..1bca58335 100644 --- a/ocsgw/Dockerfile +++ b/ocsgw/Dockerfile @@ -1,4 +1,4 @@ -FROM azul/zulu-openjdk:12.0.2 +FROM azul/zulu-openjdk:13 LABEL maintainer="dev@redotter.sg" diff --git a/ocsgw/build.gradle.kts b/ocsgw/build.gradle.kts index 12518ab58..14cb9c1e7 100644 --- a/ocsgw/build.gradle.kts +++ b/ocsgw/build.gradle.kts @@ -62,4 +62,4 @@ tasks.test { } } -apply(from = "../gradle/jacoco.gradle") \ No newline at end of file +apply(from = "../gradle/jacoco.gradle.kts") \ No newline at end of file diff --git a/ocsgw/src/main/java/org/ostelco/ocsgw/OcsApplication.java b/ocsgw/src/main/java/org/ostelco/ocsgw/OcsApplication.java index 4229a1890..e8bb63c64 100644 --- a/ocsgw/src/main/java/org/ostelco/ocsgw/OcsApplication.java +++ b/ocsgw/src/main/java/org/ostelco/ocsgw/OcsApplication.java @@ -140,7 +140,7 @@ public void doCreditControlRequest(ServerCCASession session, JCreditControlReque try { OcsServer.INSTANCE.handleRequest$ocsgw(session, request); } catch (Exception e) { - LOG.error("[><] Failure processing Credit-Control-Request [" + RequestType.getTypeAsString(request.getRequestTypeAVPValue()) + "]", e); + LOG.error("[><] Failure processing Credit-Control-Request [" + RequestType.getTypeAsString(request.getRequestTypeAVPValue()) + "] + [session.getSessionId()]", e); } break; case RequestType.EVENT_REQUEST: diff --git a/ocsgw/src/main/java/org/ostelco/ocsgw/converter/ProtobufToDiameterConverter.java b/ocsgw/src/main/java/org/ostelco/ocsgw/converter/ProtobufToDiameterConverter.java index 0e8ae8ac4..8618092a3 100644 --- a/ocsgw/src/main/java/org/ostelco/ocsgw/converter/ProtobufToDiameterConverter.java +++ b/ocsgw/src/main/java/org/ostelco/ocsgw/converter/ProtobufToDiameterConverter.java @@ -25,7 +25,9 @@ public static MultipleServiceCreditControl convertMSCC(org.ostelco.ocs.api.Multi return new MultipleServiceCreditControl( msccGRPC.getRatingGroup(), (int) msccGRPC.getServiceIdentifier(), - Collections.singletonList(new ServiceUnit()), new ServiceUnit(), new ServiceUnit(msccGRPC.getGranted().getTotalOctets(), 0, 0), + Collections.singletonList(new ServiceUnit()), + Collections.singletonList(new ServiceUnit()), + new ServiceUnit(msccGRPC.getGranted().getTotalOctets(), 0, 0), msccGRPC.getValidityTime(), msccGRPC.getQuotaHoldingTime(), msccGRPC.getVolumeQuotaThreshold(), @@ -99,12 +101,16 @@ public static CreditControlRequestInfo convertRequestToProtobuf(final CreditCont .setOutputOctets(0L)); } - ServiceUnit used = mscc.getUsed(); + for (ServiceUnit used : mscc.getUsed()) { - protoMscc.setUsed(org.ostelco.ocs.api.ServiceUnit.newBuilder() - .setInputOctets(used.getInput()) - .setOutputOctets(used.getOutput()) - .setTotalOctets(used.getTotal())); + // We do not track CC-Service-Specific-Units or CC-Time + if (used.getTotal() > 0) { + protoMscc.setUsed(org.ostelco.ocs.api.ServiceUnit.newBuilder() + .setInputOctets(used.getInput()) + .setOutputOctets(used.getOutput()) + .setTotalOctets(used.getTotal())); + } + } protoMscc.setRatingGroup(mscc.getRatingGroup()); protoMscc.setServiceIdentifier(mscc.getServiceIdentifier()); diff --git a/ocsgw/src/main/java/org/ostelco/ocsgw/datasource/local/LocalDataSource.java b/ocsgw/src/main/java/org/ostelco/ocsgw/datasource/local/LocalDataSource.java index 79937ac44..13a11f738 100644 --- a/ocsgw/src/main/java/org/ostelco/ocsgw/datasource/local/LocalDataSource.java +++ b/ocsgw/src/main/java/org/ostelco/ocsgw/datasource/local/LocalDataSource.java @@ -75,7 +75,7 @@ private CreditControlAnswer createCreditControlAnswer(CreditControlContext conte mscc.getRatingGroup(), mscc.getServiceIdentifier(), newRequested, - new ServiceUnit(mscc.getUsed().getTotal(), mscc.getUsed().getInput(), mscc.getUsed().getOutput()), + mscc.getUsed(), granted, mscc.getValidityTime(), 7200, diff --git a/ocsgw/src/main/java/org/ostelco/ocsgw/datasource/protobuf/GrpcDataSource.java b/ocsgw/src/main/java/org/ostelco/ocsgw/datasource/protobuf/GrpcDataSource.java index 2b64774d0..901951eb1 100644 --- a/ocsgw/src/main/java/org/ostelco/ocsgw/datasource/protobuf/GrpcDataSource.java +++ b/ocsgw/src/main/java/org/ostelco/ocsgw/datasource/protobuf/GrpcDataSource.java @@ -245,7 +245,7 @@ private void reconnectStreams() { if (!isReconnecting()) { reconnectStreamFuture = executorService.schedule((Callable) () -> { - LOG.debug("Reconnecting GRPC streams"); + LOG.debug("Reconnecting gRPC streams"); setupChannel(); initCreditControlRequestStream(); setupEventConsumer(); diff --git a/ocsgw/src/main/java/org/ostelco/ocsgw/datasource/protobuf/ProtobufDataSource.kt b/ocsgw/src/main/java/org/ostelco/ocsgw/datasource/protobuf/ProtobufDataSource.kt index ddf8e645f..66b47895c 100644 --- a/ocsgw/src/main/java/org/ostelco/ocsgw/datasource/protobuf/ProtobufDataSource.kt +++ b/ocsgw/src/main/java/org/ostelco/ocsgw/datasource/protobuf/ProtobufDataSource.kt @@ -116,6 +116,12 @@ class ProtobufDataSource { return } + // If CCR-I is only authentication we should block to force sync on CCR-U / CCA-U + if ((request.ccRequestType?.integer32 == CreditControlRequestType.INITIAL_REQUEST.number) && request.multipleServiceCreditControls.isEmpty()) { + blocked.add(answer.msisdn) + return + } + for (mssAnswerInfo in answer.extraInfo.msccInfoList) { for (msccRequest in request.multipleServiceCreditControls) { if (mssAnswerInfo.serviceIdentifier == msccRequest.serviceIdentifier && mssAnswerInfo.ratingGroup == msccRequest.ratingGroup) { diff --git a/ocsgw/src/main/java/org/ostelco/ocsgw/datasource/protobuf/PubSubDataSource.kt b/ocsgw/src/main/java/org/ostelco/ocsgw/datasource/protobuf/PubSubDataSource.kt index a97a14eca..ec5c5eafb 100644 --- a/ocsgw/src/main/java/org/ostelco/ocsgw/datasource/protobuf/PubSubDataSource.kt +++ b/ocsgw/src/main/java/org/ostelco/ocsgw/datasource/protobuf/PubSubDataSource.kt @@ -51,29 +51,11 @@ class PubSubDataSource( pubSubChannelProvider = FixedTransportChannelProvider.create(GrpcTransportChannel.create(channel)) } - // init publisher - logger.info("Setting up Publisher for pubsub Topic: {}", ccrTopicId) publisher = setupPublisherToTopic(projectId, ccrTopicId) + setupCcaReceiver(projectId, ccaSubscriptionId) + initCcrKeepAlive() - // Instantiate an asynchronous message receiver - setupPubSubSubscriber(projectId, ccaSubscriptionId) { message, consumer -> - // handle incoming message, then ack/nack the received message - val ccaInfo = CreditControlAnswerInfo.parseFrom(message) - if (ccaInfo.resultCode != ResultCode.UNKNOWN) { - logger.info("Pubsub received CreditControlAnswer for msisdn {} sessionId [{}]", ccaInfo.msisdn, ccaInfo.requestId) - protobufDataSource.handleCcrAnswer(ccaInfo) - } - consumer.ack() - } - - setupPubSubSubscriber(projectId, activateSubscriptionId) { message, consumer -> - // handle incoming message, then ack/nack the received message - protobufDataSource.handleActivateResponse( - ActivateResponse.parseFrom(message)) - consumer.ack() - } - - initKeepAlive() + setupActivateReceiver(projectId, activateSubscriptionId) } override fun init() { @@ -93,6 +75,27 @@ class PubSubDataSource( override fun isBlocked(msisdn: String): Boolean = protobufDataSource.isBlocked(msisdn) + + private fun setupCcaReceiver(projectId: String, ccaSubscriptionId: String) { + // Instantiate an asynchronous message receiver + setupPubSubSubscriber(projectId, ccaSubscriptionId) { message, consumer -> + val ccaInfo = CreditControlAnswerInfo.parseFrom(message) + if (ccaInfo.resultCode != ResultCode.UNKNOWN) { + logger.info("Pubsub received CreditControlAnswer for msisdn {} sessionId [{}]", ccaInfo.msisdn, ccaInfo.requestId) + protobufDataSource.handleCcrAnswer(ccaInfo) + } + consumer.ack() + } + } + + private fun setupActivateReceiver(projectId: String, activateSubscriptionId: String) { + setupPubSubSubscriber(projectId, activateSubscriptionId) { message, consumer -> + protobufDataSource.handleActivateResponse( + ActivateResponse.parseFrom(message)) + consumer.ack() + } + } + private fun sendRequest(creditControlRequestInfo : CreditControlRequestInfo) { val base64String = Base64.getEncoder().encodeToString( creditControlRequestInfo.toByteArray()) @@ -116,10 +119,14 @@ class PubSubDataSource( override fun onFailure(throwable: Throwable) { if (throwable is ApiException) { // details on the API exception - logger.warn("Status code: {}", throwable.statusCode.code) - logger.warn("Retrying: {}", throwable.isRetryable) + logger.warn("Pubsub topic: $ccaTopicId\n" + + "RequestId : ${creditControlRequestInfo.requestId} \n" + + "Message : ${throwable.message}\n" + + "Status code: ${throwable.statusCode.code}\n" + + "Retrying: ${throwable.isRetryable}") + } else { + logger.warn("Error sending CCR Request to PubSub. topic: $ccaTopicId requestId ${creditControlRequestInfo.requestId}") } - logger.warn("Error sending CCR Request to PubSub") } override fun onSuccess(messageId: String) { @@ -177,10 +184,10 @@ class PubSubDataSource( } /** - * The keep alive messages are sent so the stream is always active- + * The keep alive messages are sent so the stream is always active * This to keep latency low. */ - private fun initKeepAlive() { + private fun initCcrKeepAlive() { // this is used to keep low latency on the connection singleThreadScheduledExecutor.scheduleWithFixedDelay({ val ccrInfo = CreditControlRequestInfo.newBuilder() diff --git a/ocsgw/src/test/java/org/ostelco/ocsgw/OcsApplicationTest.java b/ocsgw/src/test/java/org/ostelco/ocsgw/OcsApplicationTest.java index 424a5b793..131c9a42b 100644 --- a/ocsgw/src/test/java/org/ostelco/ocsgw/OcsApplicationTest.java +++ b/ocsgw/src/test/java/org/ostelco/ocsgw/OcsApplicationTest.java @@ -7,6 +7,7 @@ import org.jdiameter.api.Session; import org.junit.jupiter.api.*; import org.ostelco.diameter.model.ReAuthRequestType; +import org.ostelco.diameter.model.ReportingReason; import org.ostelco.diameter.model.RequestType; import org.ostelco.diameter.model.SessionContext; import org.ostelco.diameter.test.Result; @@ -22,6 +23,8 @@ import java.nio.charset.StandardCharsets; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; import static org.junit.Assert.fail; @@ -119,7 +122,7 @@ private void simpleCreditControlRequestUpdate(Session session, session ); - TestHelper.createUpdateRequest(request.getAvps(), MSISDN, requestedBucketSize, usedBucketSize, ratingGroup, serviceIdentifier); + TestHelper.createUpdateRequest(request.getAvps(), MSISDN, requestedBucketSize, usedBucketSize, ratingGroup, serviceIdentifier, ReportingReason.QUOTA_EXHAUSTED); client.sendNextRequest(request, session); @@ -201,7 +204,7 @@ public void simpleCreditControlRequestInitUpdateNoRSU() { ); // Only report usage, no request for new bucket - TestHelper.createUpdateRequest(request.getAvps(), MSISDN, -1L, 500_000L, ratingGroup, serviceIdentifier); + TestHelper.createUpdateRequest(request.getAvps(), MSISDN, -1L, 500_000L, ratingGroup, serviceIdentifier, ReportingReason.QUOTA_EXHAUSTED); client.sendNextRequest(request, session); @@ -347,6 +350,71 @@ public void testUnknownAVP() { } } + + @Test + @DisplayName("Test no MSCC in CCR-U") + public void testNoMsccInCcrU() { + + final int ratingGroup = 10; + final int serviceIdentifier = 1; + + Session session = client.createSession(new Object() {}.getClass().getEnclosingMethod().getName()); + Request initRequest = client.createRequest( + OCS_REALM, + OCS_HOST, + session + ); + + TestHelper.createInitRequest(initRequest.getAvps(), MSISDN, 500_000L, ratingGroup, serviceIdentifier); + + client.sendNextRequest(initRequest, session); + + waitForAnswer(session.getSessionId()); + + try { + Result result = client.getAnswer(session.getSessionId()); + assertEquals(DIAMETER_SUCCESS, result.getResultCode().longValue()); + AvpSet resultAvps = result.getResultAvps(); + assertEquals(OCS_HOST, resultAvps.getAvp(Avp.ORIGIN_HOST).getUTF8String()); + assertEquals(OCS_REALM, resultAvps.getAvp(Avp.ORIGIN_REALM).getUTF8String()); + assertEquals(RequestType.INITIAL_REQUEST, resultAvps.getAvp(Avp.CC_REQUEST_TYPE).getInteger32()); + Avp resultMSCC = resultAvps.getAvp(Avp.MULTIPLE_SERVICES_CREDIT_CONTROL); + assertEquals(DIAMETER_SUCCESS, resultMSCC.getGrouped().getAvp(Avp.RESULT_CODE).getInteger32()); + assertEquals(serviceIdentifier, resultMSCC.getGrouped().getAvp(Avp.SERVICE_IDENTIFIER_CCA).getUnsigned32()); + assertEquals(ratingGroup, resultMSCC.getGrouped().getAvp(Avp.RATING_GROUP).getUnsigned32()); + Avp granted = resultMSCC.getGrouped().getAvp(Avp.GRANTED_SERVICE_UNIT); + assertEquals(500_000L, granted.getGrouped().getAvp(Avp.CC_TOTAL_OCTETS).getUnsigned64()); + } catch (AvpDataException e) { + LOG.error("Failed to get Result-Code", e); + } + + Request updateRequest = client.createRequest( + OCS_REALM, + OCS_HOST, + session + ); + + TestHelper.createUpdateRequest(updateRequest.getAvps(),MSISDN, -1L, 500_000L, ratingGroup, serviceIdentifier, ReportingReason.QHT); + + client.sendNextRequest(updateRequest, session); + + waitForAnswer(session.getSessionId()); + + try { + Result result = client.getAnswer(session.getSessionId()); + assertEquals(DIAMETER_SUCCESS, result.getResultCode().longValue()); + AvpSet resultAvps = result.getResultAvps(); + assertEquals(OCS_HOST, resultAvps.getAvp(Avp.ORIGIN_HOST).getUTF8String()); + assertEquals(OCS_REALM, resultAvps.getAvp(Avp.ORIGIN_REALM).getUTF8String()); + assertEquals(RequestType.UPDATE_REQUEST, resultAvps.getAvp(Avp.CC_REQUEST_TYPE).getInteger32()); + Avp resultMSCC = resultAvps.getAvp(Avp.MULTIPLE_SERVICES_CREDIT_CONTROL); + assertNull( "No requested MSCC", resultMSCC); + assertEquals(86400, resultAvps.getAvp(Avp.VALIDITY_TIME).getInteger32()); + } catch (AvpDataException e) { + LOG.error("Failed to get Result-Code", e); + } + } + @Test public void testReAuthRequest() { Session session = client.createSession(new Object() {}.getClass().getEnclosingMethod().getName()); diff --git a/ocsgw/src/test/java/org/ostelco/ocsgw/OcsHATest.java b/ocsgw/src/test/java/org/ostelco/ocsgw/OcsHATest.java index 81d31b93d..bdc97ce47 100644 --- a/ocsgw/src/test/java/org/ostelco/ocsgw/OcsHATest.java +++ b/ocsgw/src/test/java/org/ostelco/ocsgw/OcsHATest.java @@ -5,6 +5,7 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; +import org.ostelco.diameter.model.ReportingReason; import org.ostelco.diameter.model.RequestType; import org.ostelco.diameter.test.Result; import org.ostelco.diameter.test.TestClient; @@ -230,7 +231,7 @@ private void haCreditControlRequestUpdate(Session session, String host) { session ); - TestHelper.createUpdateRequest(request.getAvps(), MSISDN, 400000L, 500000L, 1, 10); + TestHelper.createUpdateRequest(request.getAvps(), MSISDN, 400000L, 500000L, 1, 10, ReportingReason.QUOTA_EXHAUSTED); testPGW.sendNextRequest(request, session); diff --git a/payment-processor/build.gradle.kts b/payment-processor/build.gradle.kts index d5c9aa66d..4c341fb37 100644 --- a/payment-processor/build.gradle.kts +++ b/payment-processor/build.gradle.kts @@ -42,7 +42,7 @@ configurations.named("integrationImplementation") { tasks.build.get().dependsOn(integration) -apply(from = "../gradle/jacoco.gradle") +apply(from = "../gradle/jacoco.gradle.kts") idea { module { diff --git a/payment-processor/src/main/kotlin/org/ostelco/prime/paymentprocessor/StripePaymentProcessor.kt b/payment-processor/src/main/kotlin/org/ostelco/prime/paymentprocessor/StripePaymentProcessor.kt index c0339b300..d4d2480b0 100644 --- a/payment-processor/src/main/kotlin/org/ostelco/prime/paymentprocessor/StripePaymentProcessor.kt +++ b/payment-processor/src/main/kotlin/org/ostelco/prime/paymentprocessor/StripePaymentProcessor.kt @@ -584,10 +584,7 @@ class StripePaymentProcessor : PaymentProcessor { amount = it.amount.toInt(), /* Note: 'int' is used internally for amounts. */ currency = it.currency, created = Instant.ofEpochSecond(it.created).toEpochMilli(), - refunded = it.refunded, - properties = mapOf("invoiceId" to intent.invoice, - "customerId" to intent.customer) - ) + refunded = it.refunded) } }.flatMap { it diff --git a/prime-customer-api/build.gradle.kts b/prime-customer-api/build.gradle.kts index 8ad67239c..e5751b0b7 100644 --- a/prime-customer-api/build.gradle.kts +++ b/prime-customer-api/build.gradle.kts @@ -30,8 +30,8 @@ dependencies { implementation("javax.annotation:javax.annotation-api:${Version.javaxAnnotation}") // taken from build/swagger-code-java-client/build.gradle - implementation("io.swagger:swagger-annotations:1.5.23") - implementation("com.google.code.gson:gson:2.8.5") + implementation("io.swagger:swagger-annotations:1.5.24") + implementation("com.google.code.gson:gson:${Version.gson}") implementation("com.squareup.okhttp:okhttp:2.7.5") implementation("com.squareup.okhttp:logging-interceptor:2.7.5") implementation("io.gsonfire:gson-fire:1.8.3") diff --git a/prime-modules/build.gradle.kts b/prime-modules/build.gradle.kts index ca4c82436..324c50aa9 100644 --- a/prime-modules/build.gradle.kts +++ b/prime-modules/build.gradle.kts @@ -33,4 +33,4 @@ dependencies { testImplementation(kotlin("test-junit")) } -apply(from = "../gradle/jacoco.gradle") \ No newline at end of file +apply(from = "../gradle/jacoco.gradle.kts") \ No newline at end of file diff --git a/prime-modules/src/main/kotlin/org/ostelco/prime/appnotifier/AppNotifier.kt b/prime-modules/src/main/kotlin/org/ostelco/prime/appnotifier/AppNotifier.kt index 95b110ecd..aa7cac82c 100644 --- a/prime-modules/src/main/kotlin/org/ostelco/prime/appnotifier/AppNotifier.kt +++ b/prime-modules/src/main/kotlin/org/ostelco/prime/appnotifier/AppNotifier.kt @@ -1,6 +1,29 @@ package org.ostelco.prime.appnotifier +/* Prime specific notification types. */ +enum class NotificationType { + JUMIO_VERIFICATION_SUCCEEDED, + JUMIO_VERIFICATION_FAILED, +} + interface AppNotifier { - fun notify(customerId: String, title: String, body: String) - fun notify(customerId: String, title: String, body: String, data: Map) + + /** + * Prime specific interface for sending notification messages to + * customers/clients. + * @param notificationType Type of notification to be sent. + * @param customerId Id of the receipient/customer. + * @param data Additional data to be added to the message if any. + */ + fun notify(notificationType: NotificationType, customerId: String, data: Map = emptyMap()) + + /** + * Low level interface for sending notification messages to + * customers/clients. + * @param customerId Id of the customer to receive the notification. + * @param title The 'title' part of the notification. + * @param body The message part of the notification. + * @param data Additional data to be added to the message if any. + */ + fun notify(customerId: String, title: String, body: String, data: Map = emptyMap()) } diff --git a/prime-modules/src/main/kotlin/org/ostelco/prime/paymentprocessor/core/Model.kt b/prime-modules/src/main/kotlin/org/ostelco/prime/paymentprocessor/core/Model.kt index 6b8ed3613..34977d802 100644 --- a/prime-modules/src/main/kotlin/org/ostelco/prime/paymentprocessor/core/Model.kt +++ b/prime-modules/src/main/kotlin/org/ostelco/prime/paymentprocessor/core/Model.kt @@ -32,4 +32,4 @@ data class InvoiceInfo(val id: String) data class InvoicePaymentInfo(val id: String, val chargeId: String) -data class PaymentTransactionInfo(val id: String, val amount: Int, val currency: String, val created: Long, val refunded: Boolean, val properties: Map) +data class PaymentTransactionInfo(val id: String, val amount: Int, val currency: String, val created: Long, val refunded: Boolean) 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 1f7f4771d..1c3e7c92e 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 @@ -188,7 +188,7 @@ interface ClientGraphStore { /** * Save address and Phone number */ - fun saveAddress(identity: Identity, address: String): Either + fun saveAddress(identity: Identity, address: String, regionCode: String): Either } data class ConsumptionResult(val msisdnAnalyticsId: String, val granted: Long, val balance: Long) @@ -197,7 +197,8 @@ interface AdminGraphStore { fun getCustomerForMsisdn(msisdn: String): Either - fun getIdentityForContactEmail(contactEmail: String): Either + fun getIdentityForCustomerId(id: String): Either + fun getIdentitiesFor(queryString: String): Either> /** * Link Customer to MSISDN diff --git a/prime/Dockerfile b/prime/Dockerfile index 25a304023..0c5d576a9 100644 --- a/prime/Dockerfile +++ b/prime/Dockerfile @@ -1,4 +1,4 @@ -FROM azul/zulu-openjdk:12.0.2 +FROM azul/zulu-openjdk:13 LABEL maintainer="dev@redotter.sg" diff --git a/prime/Dockerfile.test b/prime/Dockerfile.test index 5351d7467..5f3cc3f03 100644 --- a/prime/Dockerfile.test +++ b/prime/Dockerfile.test @@ -1,6 +1,6 @@ # This Dockerfile is used when running locally using docker-compose for Acceptance Testing. -FROM azul/zulu-openjdk:12.0.2 +FROM azul/zulu-openjdk:13 LABEL maintainer="dev@redotter.sg" diff --git a/prime/build.gradle.kts b/prime/build.gradle.kts index 170f2725d..57ae7dace 100644 --- a/prime/build.gradle.kts +++ b/prime/build.gradle.kts @@ -10,7 +10,7 @@ plugins { } // Update version in [script/start.sh] too. -version = "1.64.0" +version = "1.70.0" dependencies { // interface module between prime and prime-modules diff --git a/prime/cloudbuild.dev.yaml b/prime/cloudbuild.dev.yaml index 497f6e29d..0186972b2 100644 --- a/prime/cloudbuild.dev.yaml +++ b/prime/cloudbuild.dev.yaml @@ -50,7 +50,7 @@ steps: path: /root/out_zip # Build docker images - name: gcr.io/cloud-builders/docker - args: ['build', '--tag=eu.gcr.io/$PROJECT_ID/prime:$SHORT_SHA', '--cache-from', 'azul/zulu-openjdk:12.0.2', 'prime'] + args: ['build', '--tag=eu.gcr.io/$PROJECT_ID/prime:$SHORT_SHA', '--cache-from', 'azul/zulu-openjdk:13', 'prime'] timeout: 120s # Deploy new docker image to Google Kubernetes Engine (GKE) - name: ubuntu diff --git a/prime/cloudbuild.yaml b/prime/cloudbuild.yaml index 6babe6ef8..f802cd79a 100644 --- a/prime/cloudbuild.yaml +++ b/prime/cloudbuild.yaml @@ -50,7 +50,7 @@ steps: path: /root/out_zip # Build docker images - name: gcr.io/cloud-builders/docker - args: ['build', '--tag=eu.gcr.io/$PROJECT_ID/prime:$TAG_NAME', '--cache-from', 'azul/zulu-openjdk:12.0.2', 'prime'] + args: ['build', '--tag=eu.gcr.io/$PROJECT_ID/prime:$TAG_NAME', '--cache-from', 'azul/zulu-openjdk:13', 'prime'] timeout: 120s # Deploy new docker image to Google Kubernetes Engine (GKE) - name: ubuntu diff --git a/prime/config/config.yaml b/prime/config/config.yaml index b66434e05..6cff08b07 100644 --- a/prime/config/config.yaml +++ b/prime/config/config.yaml @@ -60,16 +60,26 @@ modules: config: host: ${NEO4J_HOST} protocol: bolt+routing - hssNameLookupService: - serviceInterface: org.ostelco.prime.storage.graph.HssNameLookupService - textReader: - type: file - filename: /config-data/HssNameLookupService.kts onNewCustomerAction: serviceInterface: org.ostelco.prime.storage.graph.OnNewCustomerAction textReader: type: file filename: /config-data/OnNewCustomerAction.kts + allowedRegionsService: + serviceInterface: org.ostelco.prime.storage.graph.AllowedRegionsService + textReader: + type: file + filename: /config-data/AllowedRegionsService.kts + onRegionApprovedAction: + serviceInterface: org.ostelco.prime.storage.graph.OnRegionApprovedAction + textReader: + type: file + filename: /config-data/OnRegionApprovedAction.kts + hssNameLookupService: + serviceInterface: org.ostelco.prime.storage.graph.HssNameLookupService + textReader: + type: file + filename: /config-data/HssNameLookupService.kts - type: analytics config: projectId: ${GCP_PROJECT_ID} @@ -85,25 +95,11 @@ modules: projectId: ${GCP_PROJECT_ID} activateTopicId: ${ACTIVATE_TOPIC_ID} ccrSubscriptionId: ${CCR_SUBSCRIPTION_ID} - rating: - - serviceId: 400 - ratingGroup: 400 - rate: Normal - - serviceId: 401 - ratingGroup: 401 - rate: Zero - - serviceId: 402 - ratingGroup: 402 - rate: Zero - - serviceId: 409 - ratingGroup: 409 - rate: Zero - - serviceId: -1 - ratingGroup: 600 - rate: Normal - - serviceId: 1 - ratingGroup: 10 - rate: Normal + consumptionPolicyService: + serviceInterface: org.ostelco.prime.ocs.core.ConsumptionPolicy + textReader: + type: file + filename: /config-data/ConsumptionPolicyService.kts - type: api - type: stripe-payment-processor config: diff --git a/prime/config/customer.graphqls b/prime/config/customer.graphqls index 162cd3548..f057b6a45 100644 --- a/prime/config/customer.graphqls +++ b/prime/config/customer.graphqls @@ -7,11 +7,11 @@ type QueryType { } type Context { - customer: Customer! - bundles: [Bundle!] - regions(regionCode: String): [RegionDetails!] - products: [Product!] - purchases: [Purchase!] + customer: Customer + bundles: [Bundle!]! + regions(regionCode: String): [RegionDetails!]! + products: [Product!]! + purchases: [Purchase!]! } type Customer { @@ -29,8 +29,8 @@ type Bundle { type RegionDetails { region: Region! - status: CustomerRegionStatus - kycStatusMap: KycStatusMap + status: CustomerRegionStatus! + kycStatusMap: KycStatusMap! simProfiles: [SimProfile!] } @@ -42,6 +42,7 @@ type Region { enum CustomerRegionStatus { PENDING APPROVED + AVAILABLE } type KycStatusMap { @@ -75,8 +76,8 @@ enum SimProfileStatus { type Product { sku: String! price: Price! - properties: Properties - presentation: Presentation + properties: Properties! + presentation: Presentation! } type Properties { @@ -87,11 +88,11 @@ type Properties { type Presentation { subTotal: String payeeLabel: String - priceLabel: String + priceLabel: String! taxLabel: String tax: String subTotalLabel: String - productLabel: String + productLabel: String! label: String } @@ -104,4 +105,4 @@ type Purchase { id: String! product: Product! timestamp: Long! -} \ No newline at end of file +} diff --git a/prime/config/test.yaml b/prime/config/test.yaml index 83ebc1889..5172e2451 100644 --- a/prime/config/test.yaml +++ b/prime/config/test.yaml @@ -45,16 +45,26 @@ modules: config: host: neo4j protocol: bolt - hssNameLookupService: - serviceInterface: org.ostelco.prime.storage.graph.HssNameLookupService - textReader: - type: classpathResource - filename: /HssNameLookupService.kts onNewCustomerAction: serviceInterface: org.ostelco.prime.storage.graph.OnNewCustomerAction textReader: type: classpathResource filename: /OnNewCustomerAction.kts + allowedRegionsService: + serviceInterface: org.ostelco.prime.storage.graph.AllowedRegionsService + textReader: + type: classpathResource + filename: /AllowedRegionsService.kts + onRegionApprovedAction: + serviceInterface: org.ostelco.prime.storage.graph.OnRegionApprovedAction + textReader: + type: classpathResource + filename: /OnRegionApprovedAction.kts + hssNameLookupService: + serviceInterface: org.ostelco.prime.storage.graph.HssNameLookupService + textReader: + type: classpathResource + filename: /HssNameLookupService.kts - type: analytics config: projectId: ${GCP_PROJECT_ID} @@ -70,19 +80,11 @@ modules: projectId: ${GCP_PROJECT_ID} activateTopicId: ocs-activate ccrSubscriptionId: ocs-ccr-sub - rating: - - serviceId: 1 - ratingGroup: 10 - rate: Normal - - serviceId: 2 - ratingGroup: 12 - rate: Normal - - serviceId: 4 - ratingGroup: 14 - rate: Normal - - serviceId: -1 - ratingGroup: 10 - rate: Normal + consumptionPolicyService: + serviceInterface: org.ostelco.prime.ocs.core.ConsumptionPolicy + textReader: + type: classpathResource + filename: /ConsumptionPolicyService.kts - type: api - type: firebase-app-notifier config: diff --git a/prime/infra/dev/prime-customer-api.yaml b/prime/infra/dev/prime-customer-api.yaml index ca32a6371..6dff36ed3 100644 --- a/prime/infra/dev/prime-customer-api.yaml +++ b/prime/infra/dev/prime-customer-api.yaml @@ -71,7 +71,6 @@ paths: schema: type: string security: - - auth0_jwt: [] - firebase: [] "/context": get: @@ -87,7 +86,6 @@ paths: 404: description: "Customer not found." security: - - auth0_jwt: [] - firebase: [] "/customer": get: @@ -103,7 +101,6 @@ paths: 404: description: "Customer not found." security: - - auth0_jwt: [] - firebase: [] post: description: "Create a new customer." @@ -137,7 +134,6 @@ paths: 500: description: "Failed to store customer" security: - - auth0_jwt: [] - firebase: [] put: description: "Update an existing customer." @@ -167,7 +163,6 @@ paths: 500: description: "Failed to update customer info." security: - - auth0_jwt: [] - firebase: [] delete: description: "Remove customer." @@ -180,7 +175,6 @@ paths: 404: description: "Customer not found." security: - - auth0_jwt: [] - firebase: [] "/regions": get: @@ -194,7 +188,6 @@ paths: schema: $ref: '#/definitions/RegionDetailsList' security: - - auth0_jwt: [] - firebase: [] "/regions/{regionCode}": get: @@ -216,7 +209,6 @@ paths: 404: description: "Region not found." security: - - auth0_jwt: [] - firebase: [] "/regions/{regionCode}/kyc/jumio/scans": post: @@ -238,7 +230,6 @@ paths: 404: description: "Region not found." security: - - auth0_jwt: [] - firebase: [] "/regions/{regionCode}/kyc/jumio/scans/{scanId}": get: @@ -265,7 +256,6 @@ paths: 404: description: "Region or Scan not found." security: - - auth0_jwt: [] - firebase: [] "/regions/sg/kyc/myInfoConfig": get: @@ -281,7 +271,6 @@ paths: 404: description: "Config not found." security: - - auth0_jwt: [] - firebase: [] "/regions/sg/kyc/myInfo/{authorisationCode}": get: @@ -303,7 +292,6 @@ paths: 404: description: "Person Data not found." security: - - auth0_jwt: [] - firebase: [] "/regions/sg/kyc/myInfo/v3/config": get: @@ -319,7 +307,6 @@ paths: 404: description: "Config not found." security: - - auth0_jwt: [] - firebase: [] "/regions/sg/kyc/myInfo/v3/personData/{authorisationCode}": get: @@ -341,7 +328,6 @@ paths: 404: description: "Person Data not found." security: - - auth0_jwt: [] - firebase: [] "/regions/sg/kyc/dave/{nricFinId}": get: @@ -361,14 +347,30 @@ paths: 400: description: "Invalid NRIC/FIN ID" security: - - auth0_jwt: [] - firebase: [] "/regions/sg/kyc/profile": put: - description: "Update Singapore Customer's address and phone number." + description: "Update address for Singapore region." produces: - application/json - operationId: "updateDetails" + operationId: "updateDetailsForSG" + parameters: + - name: address + in: query + description: "Customer's Address" + required: true + type: string + responses: + 204: + description: "Successfully updated customer's details." + security: + - firebase: [] + "/regions/my/kyc/profile": + put: + description: "Update address for Malaysia region." + produces: + - application/json + operationId: "updateDetailsForMY" parameters: - name: address in: query @@ -379,7 +381,6 @@ paths: 204: description: "Successfully updated customer's details." security: - - auth0_jwt: [] - firebase: [] "/regions/{regionCode}/simProfiles": get: @@ -403,7 +404,6 @@ paths: 500: description: "Service Unavailable" security: - - auth0_jwt: [] - firebase: [] post: description: "Provision SIM Profile for the user (identified by bearer token)." @@ -430,7 +430,6 @@ paths: 500: description: "Service Unavailable" security: - - auth0_jwt: [] - firebase: [] "/regions/{regionCode}/simProfiles/{iccId}": put: @@ -488,7 +487,6 @@ paths: 500: description: "Service Unavailable" security: - - auth0_jwt: [] - firebase: [] "/regions/{regionCode}/subscriptions": get: @@ -510,7 +508,6 @@ paths: 404: description: "No subscription found for this user." security: - - auth0_jwt: [] - firebase: [] "/subscriptions": get: @@ -526,7 +523,6 @@ paths: 404: description: "No subscription found for this user." security: - - auth0_jwt: [] - firebase: [] "/applicationToken": post: @@ -554,7 +550,6 @@ paths: 500: description: "Not able to store token." security: - - auth0_jwt: [] - firebase: [] "/paymentSources": get: @@ -572,7 +567,6 @@ paths: 503: description: "Service Unavailable" security: - - auth0_jwt: [] - firebase: [] post: description: "Add a new payment source for user" @@ -597,7 +591,6 @@ paths: 500: description: "Service Unavailable" security: - - auth0_jwt: [] - firebase: [] put: description: "Set the source as default for user" @@ -622,7 +615,6 @@ paths: 500: description: "Service Unavailable" security: - - auth0_jwt: [] - firebase: [] delete: description: "Remove a payment source for user" @@ -647,7 +639,6 @@ paths: 500: description: "Service Unavailable" security: - - auth0_jwt: [] - firebase: [] "/products": get: @@ -663,7 +654,6 @@ paths: 404: description: "No products found for the user." security: - - auth0_jwt: [] - firebase: [] "/products/{sku}/purchase": post: @@ -696,7 +686,6 @@ paths: 404: description: "Product not found." security: - - auth0_jwt: [] - firebase: [] "/purchases": get: @@ -717,7 +706,6 @@ paths: 500: description: "Service Unavailable" security: - - auth0_jwt: [] - firebase: [] "/bundles": get: @@ -733,7 +721,6 @@ paths: 404: description: "No bundle found for this user." security: - - auth0_jwt: [] - firebase: [] "/referred": get: @@ -749,7 +736,6 @@ paths: 404: description: "No referrals found for this user." security: - - auth0_jwt: [] - firebase: [] "/referred/by": get: @@ -765,7 +751,6 @@ paths: 404: description: "No 'referred by' found for this user." security: - - auth0_jwt: [] - firebase: [] "/graphql": get: @@ -787,7 +772,6 @@ paths: 404: description: "Not found" security: - - auth0_jwt: [] - firebase: [] post: description: "GraphQL POST endpoint" @@ -810,7 +794,6 @@ paths: 404: description: "Not found" security: - - auth0_jwt: [] - firebase: [] definitions: @@ -875,7 +858,7 @@ definitions: status: description: "Customer Status for this region" type: string - enum: [ PENDING, APPROVED ] + enum: [ PENDING, APPROVED, AVAILABLE ] kycStatusMap: description: "Map of status for each KYC" type: object diff --git a/prime/infra/dev/prime-houston-api.yaml b/prime/infra/dev/prime-houston-api.yaml index da13a8d5e..e815a2d45 100644 --- a/prime/infra/dev/prime-houston-api.yaml +++ b/prime/infra/dev/prime-houston-api.yaml @@ -2,7 +2,7 @@ swagger: "2.0" info: title: "Houston Admin API" description: "The APIs for the Houston Admin Client." - version: "1.0.0" + version: "2.0.0" host: "houston-api.dev.oya.world" x-google-endpoints: - name: "houston-api.dev.oya.world" @@ -10,28 +10,28 @@ x-google-endpoints: schemes: - "https" paths: - "/support/profiles/{id}": + "/support/profiles/{query}": get: - description: "Get profile for the given email-id or msisdn (url encoded)." + description: "Get list of customers for the given email-id or msisdn (url encoded)." produces: - application/json - operationId: "getCustomer" + operationId: "getCustomerList" responses: 200: description: "Get the profile for this user." schema: - $ref: '#/definitions/Profile' + $ref: '#/definitions/CustomerList' 404: description: "Profile not found." security: - auth0_jwt: [] parameters: - - name: id + - name: query in: path description: "The id of the user (msisdn or email)" required: true type: string - "/support/profiles/{email}/subscriptions": + "/support/profiles/{id}/subscriptions": get: description: "Get subscription (msisdn) for the user." produces: @@ -47,12 +47,12 @@ paths: security: - auth0_jwt: [] parameters: - - name: email + - name: id in: path - description: "The email of the user" + description: "The customerId of the user" required: true type: string - "/support/profiles/{email}/scans": + "/support/profiles/{id}/scans": get: description: "Get eKYC scan information for the user." produces: @@ -68,87 +68,17 @@ paths: security: - auth0_jwt: [] parameters: - - name: email - in: path - description: "The email of the user" - required: true - type: string - "/support/profiles/{email}/plans": - get: - description: "Get all plans subscribed to by a customer" - produces: - - application/json - operationId: "getPlans" - security: - - auth0_jwt: [] - responses: - 200: - description: "Plans subscribed to" - schema: - $ref: '#/definitions/PlanList' - 404: - description: "No plans found" - parameters: - - name: email - in: path - description: "The email of the customer" - required: true - type: string - "/support/profiles/{email}/plans/{planId}": - post: - description: "Subscribe a customer to a plan" - produces: - - application/json - - text/plain - operationId: "attachPlan" - responses: - 201: - description: "The subscription was created successfully" - 400: - description: "Failed to create subscription" - security: - - auth0_jwt: [] - parameters: - - name: email - in: path - description: "The email of the customer" - required: true - type: string - - name: planId - in: path - description: "The name of the plan to subscribe to" - required: true - type: string - delete: - description: "Remove a customer from a plan" - produces: - - application/json - - text/plain - operationId: "detachPlan" - responses: - 200: - description: "The subscription was removed successfully" - 400: - description: "Failed to remove subscription" - security: - - auth0_jwt: [] - parameters: - - name: email + - name: id in: path - description: "The email of the customer" + description: "The customerId of the user" required: true type: string - - name: planId - in: path - description: "The name of the plan to remove the subscription for" - required: true - type: string - "/support/bundles/{email}": + "/support/bundles/{id}": get: description: "Get bundles (balance) for the user (identified by email)." produces: - application/json - operationId: "getBundlesByEmail" + operationId: "getBundlesById" responses: 200: description: "Get bundles for this user." @@ -159,18 +89,18 @@ paths: security: - auth0_jwt: [] parameters: - - name: email + - name: id in: path - description: "The email of the user" + description: "The customerId of the user" required: true type: string - "/support/purchases/{email}": + "/support/purchases/{id}": get: description: "Get list of all purchases." produces: - application/json - text/plain - operationId: "getPurchaseHistoryByEmail" + operationId: "getPurchaseHistoryById" responses: 200: description: "List of Purchase Records." @@ -185,18 +115,18 @@ paths: security: - auth0_jwt: [] parameters: - - name: email + - name: id in: path - description: "The email of the user" + description: "The customerId of the user" required: true type: string - "/support/refund/{email}": + "/support/refund/{id}": put: description: "Full refund of a purchase." produces: - application/json - text/plain - operationId: "refundPurchaseByEmail" + operationId: "refundPurchaseById" responses: 200: description: "Purchase is refunded." @@ -211,9 +141,9 @@ paths: security: - auth0_jwt: [] parameters: - - name: email + - name: id in: path - description: "The email of the user" + description: "The customerId of the user" required: true type: string - name: purchaseRecordId @@ -226,13 +156,13 @@ paths: description: "The reason for refund" required: true type: string - "/support/notify/{email}": + "/support/notify/{id}": put: description: "Send notification to a customer." produces: - application/json - text/plain - operationId: "sendNotificationByEmail" + operationId: "sendNotificationById" responses: 200: description: "Sent notification." @@ -243,9 +173,9 @@ paths: security: - auth0_jwt: [] parameters: - - name: email + - name: id in: path - description: "The email of the user" + description: "The customerId of the user" required: true type: string - name: title @@ -258,99 +188,12 @@ paths: description: "The notification message" required: true type: string - "/support/plans": - get: - description: "Get plan details" - produces: - - application/json - - text/plain - operationId: "getPlan" - responses: - 200: - description: "Plan details is returned" - schema: - $ref: '#/definitions/Plan' - 404: - description: "No such plan" - parameters: - - name: planId - in: path - description: "Name of plan to get" - required: true - type: string - security: - - auth0_jwt: [] - post: - description: "Create a plan" - produces: - - application/json - - text/plain - operationId: "createPlan" - responses: - 201: - description: "Successfully purchased the plan." - schema: - $ref: '#/definitions/Plan' - 400: - description: "Failed to create the plan" - parameters: - - name: createPlanRequest - in: body - description: Plan details - schema: - $ref: '#/definitions/CreatePlanRequest' - security: - - auth0_jwt: [] - delete: - description: "Removes a plan" - produces: - - application/json - - text/plain - operationId: "deletePlan" - responses: - 200: - description: "Plan is removed" - schema: - $ref: '#/definitions/Plan' - 400: - description: "Failed to remove plan" - 404: - description: "No such plan" - parameters: - - name: planId - in: path - description: "The name of the plan to remove" - required: true - type: string - security: - - auth0_jwt: [] - "/support/profiles/{email}/state": - get: - description: "Get state of the user." - produces: - - application/json - operationId: "getCustomerState" - responses: - 200: - description: "Successfully retrieved the state." - schema: - $ref: '#/definitions/SubscriberState' - 404: - description: "No state information available for this user." - security: - - auth0_jwt: [] - parameters: - - name: email - in: path - description: "The email of the user" - required: true - type: string - "/support/context/{email}": + "/support/context/{id}": get: description: "Get context which is customer and region details." produces: - application/json - operationId: "getContext" + operationId: "getContextById" responses: 200: description: "Get the customer context." @@ -361,18 +204,18 @@ paths: security: - auth0_jwt: [] parameters: - - name: email + - name: id in: path - description: "The email of the user" + description: "The customerId of the user" required: true type: string - "/support/auditLog/{email}": + "/support/auditLog/{id}": get: description: "Get list of all audit logs." produces: - application/json - text/plain - operationId: "getAuditLogsByEmail" + operationId: "queryAuditLogs" responses: 200: description: "List of AuditLog records." @@ -383,12 +226,12 @@ paths: security: - auth0_jwt: [] parameters: - - name: email + - name: id in: path - description: "The email of the user" + description: "The customerId of the user" required: true type: string - "/support/customer/{email}": + "/support/customer/{id}": delete: description: "Remove the customer from the backend." produces: @@ -402,28 +245,13 @@ paths: security: - auth0_jwt: [] parameters: - - name: email + - name: id in: path - description: "The contact email of the customer" + description: "The customerId of the customer" required: true type: string definitions: - Pong: - type: object - properties: - timestamp: - type: integer - format: int64 - required: - - timestamp - Uuid: - type: object - properties: - uuid: - type: string - required: - - uuid Context: type: object properties: @@ -431,6 +259,10 @@ definitions: $ref: '#/definitions/Customer' regions: $ref: '#/definitions/RegionDetailsList' + CustomerList: + type: array + items: + $ref: '#/definitions/Customer' Customer: type: object properties: @@ -446,8 +278,8 @@ definitions: referralId: type: string required: - - name - - email + - id + - contactEmail RegionDetailsList: type: array items: @@ -482,12 +314,6 @@ definitions: KycStatus: type: string enum: [ PENDING, REJECTED, APPROVED ] - MyInfoConfig: - type: object - properties: - url: - type: string - description: "URL for MyInfo authorise" Region: type: object properties: @@ -519,26 +345,6 @@ definitions: - iccId - activationCode - status - Profile: - type: object - properties: - name: - type: string - address: - type: string - postCode: - type: string - city: - type: string - country: - type: string - email: - type: string - format: email - referralId: - type: string - required: - - email SubscriptionList: type: array items: @@ -638,60 +444,6 @@ definitions: required: - amount - currency - Plan: - type: object - properties: - id: - description: "An unique id representing the plan" - type: string - stripePlanId: - type: string - stripeProductId: - type: string - interval: - description: "The recurring period for the plan" - type: string - enum: [ day, week, month, year ] - intervalCount: - description: "Number of intervals in a period" - type: integer - default: 1 - minimum: 1 - required: - - id - PlanList: - type: array - items: - $ref: '#/definitions/Plan' - CreatePlanRequest: - type: object - properties: - plan: - description: Plan to be created. - $ref: '#/definitions/Plan' - stripeProductName: - description: Name of product in Stripe for this plan. - type: string - productPlan: - description: Product associated with this plan. - $ref: '#/definitions/Product' - SubscriberState: - type: object - properties: - id: - description: "User Id" - type: string - status: - description: "Current status of the customer" - type: string - modifiedTimestamp: - description: "Last modified time for the status (Unix timestamp)" - type: integer - format: int64 - required: - - id - - status - - modifiedTimestamp ScanInformationList: type: array items: diff --git a/prime/infra/prime-direct-values.yaml b/prime/infra/prime-direct-values.yaml index 75a91c84d..731194964 100644 --- a/prime/infra/prime-direct-values.yaml +++ b/prime/infra/prime-direct-values.yaml @@ -11,6 +11,8 @@ podAutoscaling: enabled: false cronjobs: + shredder: + enabled: false prime: image: eu.gcr.io/pi-ostelco-dev/prime @@ -133,8 +135,10 @@ prime: cpu: 100m memory: 768Mi livenessProbe: {} - readinessProbe: {} - annotations: + readinessProbe: + path: /ping + port: 8080 + annotations: prometheus.io/scrape: 'true' prometheus.io/path: '/prometheus-metrics' prometheus.io/port: '8081' diff --git a/prime/infra/prod/prime-customer-api.yaml b/prime/infra/prod/prime-customer-api.yaml index 410960ea9..3edb5896d 100644 --- a/prime/infra/prod/prime-customer-api.yaml +++ b/prime/infra/prod/prime-customer-api.yaml @@ -71,7 +71,6 @@ paths: schema: type: string security: - - auth0_jwt: [] - firebase: [] "/context": get: @@ -87,7 +86,6 @@ paths: 404: description: "Customer not found." security: - - auth0_jwt: [] - firebase: [] "/customer": get: @@ -103,7 +101,6 @@ paths: 404: description: "Customer not found." security: - - auth0_jwt: [] - firebase: [] post: description: "Create a new customer." @@ -137,7 +134,6 @@ paths: 500: description: "Failed to store customer" security: - - auth0_jwt: [] - firebase: [] put: description: "Update an existing customer." @@ -167,7 +163,6 @@ paths: 500: description: "Failed to update customer info." security: - - auth0_jwt: [] - firebase: [] delete: description: "Remove customer." @@ -180,7 +175,6 @@ paths: 404: description: "Customer not found." security: - - auth0_jwt: [] - firebase: [] "/regions": get: @@ -194,7 +188,6 @@ paths: schema: $ref: '#/definitions/RegionDetailsList' security: - - auth0_jwt: [] - firebase: [] "/regions/{regionCode}": get: @@ -216,7 +209,6 @@ paths: 404: description: "Region not found." security: - - auth0_jwt: [] - firebase: [] "/regions/{regionCode}/kyc/jumio/scans": post: @@ -238,7 +230,6 @@ paths: 404: description: "Region not found." security: - - auth0_jwt: [] - firebase: [] "/regions/{regionCode}/kyc/jumio/scans/{scanId}": get: @@ -265,7 +256,6 @@ paths: 404: description: "Region or Scan not found." security: - - auth0_jwt: [] - firebase: [] "/regions/sg/kyc/myInfoConfig": get: @@ -281,7 +271,6 @@ paths: 404: description: "Config not found." security: - - auth0_jwt: [] - firebase: [] "/regions/sg/kyc/myInfo/{authorisationCode}": get: @@ -303,7 +292,6 @@ paths: 404: description: "Person Data not found." security: - - auth0_jwt: [] - firebase: [] "/regions/sg/kyc/myInfo/v3/config": get: @@ -319,7 +307,6 @@ paths: 404: description: "Config not found." security: - - auth0_jwt: [] - firebase: [] "/regions/sg/kyc/myInfo/v3/personData/{authorisationCode}": get: @@ -341,7 +328,6 @@ paths: 404: description: "Person Data not found." security: - - auth0_jwt: [] - firebase: [] "/regions/sg/kyc/dave/{nricFinId}": get: @@ -361,14 +347,30 @@ paths: 400: description: "Invalid NRIC/FIN ID" security: - - auth0_jwt: [] - firebase: [] "/regions/sg/kyc/profile": put: - description: "Update Singapore Customer's address and phone number." + description: "Update address for Singapore region." produces: - application/json - operationId: "updateDetails" + operationId: "updateDetailsForSG" + parameters: + - name: address + in: query + description: "Customer's Address" + required: true + type: string + responses: + 204: + description: "Successfully updated customer's details." + security: + - firebase: [] + "/regions/my/kyc/profile": + put: + description: "Update address for Malaysia region." + produces: + - application/json + operationId: "updateDetailsForMY" parameters: - name: address in: query @@ -379,7 +381,6 @@ paths: 204: description: "Successfully updated customer's details." security: - - auth0_jwt: [] - firebase: [] "/regions/{regionCode}/simProfiles": get: @@ -403,7 +404,6 @@ paths: 500: description: "Service Unavailable" security: - - auth0_jwt: [] - firebase: [] post: description: "Provision SIM Profile for the user (identified by bearer token)." @@ -430,7 +430,6 @@ paths: 500: description: "Service Unavailable" security: - - auth0_jwt: [] - firebase: [] "/regions/{regionCode}/simProfiles/{iccId}": put: @@ -488,7 +487,6 @@ paths: 500: description: "Service Unavailable" security: - - auth0_jwt: [] - firebase: [] "/regions/{regionCode}/subscriptions": get: @@ -510,7 +508,6 @@ paths: 404: description: "No subscription found for this user." security: - - auth0_jwt: [] - firebase: [] "/subscriptions": get: @@ -526,7 +523,6 @@ paths: 404: description: "No subscription found for this user." security: - - auth0_jwt: [] - firebase: [] "/applicationToken": post: @@ -554,7 +550,6 @@ paths: 500: description: "Not able to store token." security: - - auth0_jwt: [] - firebase: [] "/paymentSources": get: @@ -572,7 +567,6 @@ paths: 503: description: "Service Unavailable" security: - - auth0_jwt: [] - firebase: [] post: description: "Add a new payment source for user" @@ -597,7 +591,6 @@ paths: 500: description: "Service Unavailable" security: - - auth0_jwt: [] - firebase: [] put: description: "Set the source as default for user" @@ -622,7 +615,6 @@ paths: 500: description: "Service Unavailable" security: - - auth0_jwt: [] - firebase: [] delete: description: "Remove a payment source for user" @@ -647,7 +639,6 @@ paths: 500: description: "Service Unavailable" security: - - auth0_jwt: [] - firebase: [] "/products": get: @@ -663,7 +654,6 @@ paths: 404: description: "No products found for the user." security: - - auth0_jwt: [] - firebase: [] "/products/{sku}/purchase": post: @@ -696,7 +686,6 @@ paths: 404: description: "Product not found." security: - - auth0_jwt: [] - firebase: [] "/purchases": get: @@ -717,7 +706,6 @@ paths: 500: description: "Service Unavailable" security: - - auth0_jwt: [] - firebase: [] "/bundles": get: @@ -733,7 +721,6 @@ paths: 404: description: "No bundle found for this user." security: - - auth0_jwt: [] - firebase: [] "/referred": get: @@ -749,7 +736,6 @@ paths: 404: description: "No referrals found for this user." security: - - auth0_jwt: [] - firebase: [] "/referred/by": get: @@ -765,7 +751,6 @@ paths: 404: description: "No 'referred by' found for this user." security: - - auth0_jwt: [] - firebase: [] "/graphql": get: @@ -787,7 +772,6 @@ paths: 404: description: "Not found" security: - - auth0_jwt: [] - firebase: [] post: description: "GraphQL POST endpoint" @@ -810,7 +794,6 @@ paths: 404: description: "Not found" security: - - auth0_jwt: [] - firebase: [] definitions: @@ -875,7 +858,7 @@ definitions: status: description: "Customer Status for this region" type: string - enum: [ PENDING, APPROVED ] + enum: [ PENDING, APPROVED, AVAILABLE ] kycStatusMap: description: "Map of status for each KYC" type: object diff --git a/prime/infra/prod/prime-houston-api.yaml b/prime/infra/prod/prime-houston-api.yaml index 81d2918ca..46640bf78 100644 --- a/prime/infra/prod/prime-houston-api.yaml +++ b/prime/infra/prod/prime-houston-api.yaml @@ -2,7 +2,7 @@ swagger: "2.0" info: title: "Houston Admin API" description: "The APIs for the Houston Admin Client." - version: "1.0.0" + version: "2.0.0" host: "houston-api.oya.world" x-google-endpoints: - name: "houston-api.oya.world" @@ -10,28 +10,28 @@ x-google-endpoints: schemes: - "https" paths: - "/support/profiles/{id}": + "/support/profiles/{query}": get: - description: "Get profile for the given email-id or msisdn (url encoded)." + description: "Get list of customers for the given email-id or msisdn (url encoded)." produces: - application/json - operationId: "getCustomer" + operationId: "getCustomerList" responses: 200: description: "Get the profile for this user." schema: - $ref: '#/definitions/Profile' + $ref: '#/definitions/CustomerList' 404: description: "Profile not found." security: - auth0_jwt: [] parameters: - - name: id + - name: query in: path description: "The id of the user (msisdn or email)" required: true type: string - "/support/profiles/{email}/subscriptions": + "/support/profiles/{id}/subscriptions": get: description: "Get subscription (msisdn) for the user." produces: @@ -47,12 +47,12 @@ paths: security: - auth0_jwt: [] parameters: - - name: email + - name: id in: path - description: "The email of the user" + description: "The customerId of the user" required: true type: string - "/support/profiles/{email}/scans": + "/support/profiles/{id}/scans": get: description: "Get eKYC scan information for the user." produces: @@ -68,87 +68,17 @@ paths: security: - auth0_jwt: [] parameters: - - name: email - in: path - description: "The email of the user" - required: true - type: string - "/support/profiles/{email}/plans": - get: - description: "Get all plans subscribed to by a customer" - produces: - - application/json - operationId: "getPlans" - security: - - auth0_jwt: [] - responses: - 200: - description: "Plans subscribed to" - schema: - $ref: '#/definitions/PlanList' - 404: - description: "No plans found" - parameters: - - name: email - in: path - description: "The email of the customer" - required: true - type: string - "/support/profiles/{email}/plans/{planId}": - post: - description: "Subscribe a customer to a plan" - produces: - - application/json - - text/plain - operationId: "attachPlan" - responses: - 201: - description: "The subscription was created successfully" - 400: - description: "Failed to create subscription" - security: - - auth0_jwt: [] - parameters: - - name: email - in: path - description: "The email of the customer" - required: true - type: string - - name: planId - in: path - description: "The name of the plan to subscribe to" - required: true - type: string - delete: - description: "Remove a customer from a plan" - produces: - - application/json - - text/plain - operationId: "detachPlan" - responses: - 200: - description: "The subscription was removed successfully" - 400: - description: "Failed to remove subscription" - security: - - auth0_jwt: [] - parameters: - - name: email + - name: id in: path - description: "The email of the customer" + description: "The customerId of the user" required: true type: string - - name: planId - in: path - description: "The name of the plan to remove the subscription for" - required: true - type: string - "/support/bundles/{email}": + "/support/bundles/{id}": get: description: "Get bundles (balance) for the user (identified by email)." produces: - application/json - operationId: "getBundlesByEmail" + operationId: "getBundlesById" responses: 200: description: "Get bundles for this user." @@ -159,18 +89,18 @@ paths: security: - auth0_jwt: [] parameters: - - name: email + - name: id in: path - description: "The email of the user" + description: "The customerId of the user" required: true type: string - "/support/purchases/{email}": + "/support/purchases/{id}": get: description: "Get list of all purchases." produces: - application/json - text/plain - operationId: "getPurchaseHistoryByEmail" + operationId: "getPurchaseHistoryById" responses: 200: description: "List of Purchase Records." @@ -185,18 +115,18 @@ paths: security: - auth0_jwt: [] parameters: - - name: email + - name: id in: path - description: "The email of the user" + description: "The customerId of the user" required: true type: string - "/support/refund/{email}": + "/support/refund/{id}": put: description: "Full refund of a purchase." produces: - application/json - text/plain - operationId: "refundPurchaseByEmail" + operationId: "refundPurchaseById" responses: 200: description: "Purchase is refunded." @@ -211,9 +141,9 @@ paths: security: - auth0_jwt: [] parameters: - - name: email + - name: id in: path - description: "The email of the user" + description: "The customerId of the user" required: true type: string - name: purchaseRecordId @@ -226,13 +156,13 @@ paths: description: "The reason for refund" required: true type: string - "/support/notify/{email}": + "/support/notify/{id}": put: description: "Send notification to a customer." produces: - application/json - text/plain - operationId: "sendNotificationByEmail" + operationId: "sendNotificationById" responses: 200: description: "Sent notification." @@ -243,9 +173,9 @@ paths: security: - auth0_jwt: [] parameters: - - name: email + - name: id in: path - description: "The email of the user" + description: "The customerId of the user" required: true type: string - name: title @@ -258,99 +188,12 @@ paths: description: "The notification message" required: true type: string - "/support/plans": - get: - description: "Get plan details" - produces: - - application/json - - text/plain - operationId: "getPlan" - responses: - 200: - description: "Plan details is returned" - schema: - $ref: '#/definitions/Plan' - 404: - description: "No such plan" - parameters: - - name: planId - in: path - description: "Name of plan to get" - required: true - type: string - security: - - auth0_jwt: [] - post: - description: "Create a plan" - produces: - - application/json - - text/plain - operationId: "createPlan" - responses: - 201: - description: "Successfully purchased the plan." - schema: - $ref: '#/definitions/Plan' - 400: - description: "Failed to create the plan" - parameters: - - name: createPlanRequest - in: body - description: Plan details - schema: - $ref: '#/definitions/CreatePlanRequest' - security: - - auth0_jwt: [] - delete: - description: "Removes a plan" - produces: - - application/json - - text/plain - operationId: "deletePlan" - responses: - 200: - description: "Plan is removed" - schema: - $ref: '#/definitions/Plan' - 400: - description: "Failed to remove plan" - 404: - description: "No such plan" - parameters: - - name: planId - in: path - description: "The name of the plan to remove" - required: true - type: string - security: - - auth0_jwt: [] - "/support/profiles/{email}/state": - get: - description: "Get state of the user." - produces: - - application/json - operationId: "getCustomerState" - responses: - 200: - description: "Successfully retrieved the state." - schema: - $ref: '#/definitions/SubscriberState' - 404: - description: "No state information available for this user." - security: - - auth0_jwt: [] - parameters: - - name: email - in: path - description: "The email of the user" - required: true - type: string - "/support/context/{email}": + "/support/context/{id}": get: description: "Get context which is customer and region details." produces: - application/json - operationId: "getContext" + operationId: "getContextById" responses: 200: description: "Get the customer context." @@ -361,18 +204,18 @@ paths: security: - auth0_jwt: [] parameters: - - name: email + - name: id in: path - description: "The email of the user" + description: "The customerId of the user" required: true type: string - "/support/auditLog/{email}": + "/support/auditLog/{id}": get: description: "Get list of all audit logs." produces: - application/json - text/plain - operationId: "getAuditLogsByEmail" + operationId: "queryAuditLogs" responses: 200: description: "List of AuditLog records." @@ -383,12 +226,12 @@ paths: security: - auth0_jwt: [] parameters: - - name: email + - name: id in: path - description: "The email of the user" + description: "The customerId of the user" required: true type: string - "/support/customer/{email}": + "/support/customer/{id}": delete: description: "Remove the customer from the backend." produces: @@ -402,28 +245,13 @@ paths: security: - auth0_jwt: [] parameters: - - name: email + - name: id in: path - description: "The contact email of the customer" + description: "The customerId of the customer" required: true type: string definitions: - Pong: - type: object - properties: - timestamp: - type: integer - format: int64 - required: - - timestamp - Uuid: - type: object - properties: - uuid: - type: string - required: - - uuid Context: type: object properties: @@ -431,6 +259,10 @@ definitions: $ref: '#/definitions/Customer' regions: $ref: '#/definitions/RegionDetailsList' + CustomerList: + type: array + items: + $ref: '#/definitions/Customer' Customer: type: object properties: @@ -446,8 +278,8 @@ definitions: referralId: type: string required: - - name - - email + - id + - contactEmail RegionDetailsList: type: array items: @@ -482,12 +314,6 @@ definitions: KycStatus: type: string enum: [ PENDING, REJECTED, APPROVED ] - MyInfoConfig: - type: object - properties: - url: - type: string - description: "URL for MyInfo authorise" Region: type: object properties: @@ -519,26 +345,6 @@ definitions: - iccId - activationCode - status - Profile: - type: object - properties: - name: - type: string - address: - type: string - postCode: - type: string - city: - type: string - country: - type: string - email: - type: string - format: email - referralId: - type: string - required: - - email SubscriptionList: type: array items: @@ -638,60 +444,6 @@ definitions: required: - amount - currency - Plan: - type: object - properties: - id: - description: "An unique id representing the plan" - type: string - stripePlanId: - type: string - stripeProductId: - type: string - interval: - description: "The recurring period for the plan" - type: string - enum: [ day, week, month, year ] - intervalCount: - description: "Number of intervals in a period" - type: integer - default: 1 - minimum: 1 - required: - - id - PlanList: - type: array - items: - $ref: '#/definitions/Plan' - CreatePlanRequest: - type: object - properties: - plan: - description: Plan to be created. - $ref: '#/definitions/Plan' - stripeProductName: - description: Name of product in Stripe for this plan. - type: string - productPlan: - description: Product associated with this plan. - $ref: '#/definitions/Product' - SubscriberState: - type: object - properties: - id: - description: "User Id" - type: string - status: - description: "Current status of the customer" - type: string - modifiedTimestamp: - description: "Last modified time for the status (Unix timestamp)" - type: integer - format: int64 - required: - - id - - status - - modifiedTimestamp ScanInformationList: type: array items: diff --git a/prime/script/start.sh b/prime/script/start.sh index 7310187fa..3c1f7e6d4 100755 --- a/prime/script/start.sh +++ b/prime/script/start.sh @@ -5,5 +5,5 @@ exec java \ -Dfile.encoding=UTF-8 \ --add-opens java.base/java.lang=ALL-UNNAMED \ --add-opens java.base/java.io=ALL-UNNAMED \ - -agentpath:/opt/cprof/profiler_java_agent.so=-cprof_service=prime,-cprof_service_version=1.64.0,-logtostderr,-minloglevel=2,-cprof_enable_heap_sampling \ + -agentpath:/opt/cprof/profiler_java_agent.so=-cprof_service=prime,-cprof_service_version=1.70.0,-logtostderr,-minloglevel=2,-cprof_enable_heap_sampling \ -jar /prime.jar server /config/config.yaml diff --git a/prime/src/integration-test/kotlin/org/ostelco/prime/storage/graph/Neo4jStorageTest.kt b/prime/src/integration-test/kotlin/org/ostelco/prime/storage/graph/Neo4jStorageTest.kt index a82db2bf9..b1df9ec73 100644 --- a/prime/src/integration-test/kotlin/org/ostelco/prime/storage/graph/Neo4jStorageTest.kt +++ b/prime/src/integration-test/kotlin/org/ostelco/prime/storage/graph/Neo4jStorageTest.kt @@ -125,17 +125,29 @@ class Neo4jStorageTest { ConfigRegistry.config = Config( host = "0.0.0.0", protocol = "bolt", - hssNameLookupService = KtsServiceFactory( - serviceInterface = "org.ostelco.prime.storage.graph.HssNameLookupService", - textReader = ClasspathResourceTextReader( - filename = "/HssNameLookupService.kts" - ) - ), onNewCustomerAction = KtsServiceFactory( serviceInterface = "org.ostelco.prime.storage.graph.OnNewCustomerAction", textReader = ClasspathResourceTextReader( filename = "/OnNewCustomerAction.kts" ) + ), + allowedRegionsService = KtsServiceFactory( + serviceInterface = "org.ostelco.prime.storage.graph.AllowedRegionsService", + textReader = ClasspathResourceTextReader( + filename = "/AllowedRegionsService.kts" + ) + ), + onRegionApprovedAction = KtsServiceFactory( + serviceInterface = "org.ostelco.prime.storage.graph.OnRegionApprovedAction", + textReader = ClasspathResourceTextReader( + filename = "/OnRegionApprovedAction.kts" + ) + ), + hssNameLookupService = KtsServiceFactory( + serviceInterface = "org.ostelco.prime.storage.graph.HssNameLookupService", + textReader = ClasspathResourceTextReader( + filename = "/HssNameLookupService.kts" + ) ) ) diff --git a/prime/src/integration-test/resources/config.yaml b/prime/src/integration-test/resources/config.yaml index fce75f97a..2d5ea09c6 100644 --- a/prime/src/integration-test/resources/config.yaml +++ b/prime/src/integration-test/resources/config.yaml @@ -8,16 +8,26 @@ modules: config: host: 0.0.0.0 protocol: bolt - hssNameLookupService: - serviceInterface: org.ostelco.prime.storage.graph.HssNameLookupService - textReader: - type: classpathResource - filename: /HssNameLookupService.kts onNewCustomerAction: serviceInterface: org.ostelco.prime.storage.graph.OnNewCustomerAction textReader: type: classpathResource filename: /OnNewCustomerAction.kts + allowedRegionsService: + serviceInterface: org.ostelco.prime.storage.graph.AllowedRegionsService + textReader: + type: classpathResource + filename: /AllowedRegionsService.kts + onRegionApprovedAction: + serviceInterface: org.ostelco.prime.storage.graph.OnRegionApprovedAction + textReader: + type: classpathResource + filename: /OnRegionApprovedAction.kts + hssNameLookupService: + serviceInterface: org.ostelco.prime.storage.graph.HssNameLookupService + textReader: + type: classpathResource + filename: /HssNameLookupService.kts - type: analytics config: projectId: ${GCP_PROJECT_ID} @@ -29,6 +39,11 @@ modules: - type: ocs config: lowBalanceThreshold: 0 + consumptionPolicyService: + serviceInterface: org.ostelco.prime.ocs.core.ConsumptionPolicy + textReader: + type: classpathResource + filename: /ConsumptionPolicyService.kts - type: api - type: stripe-payment-processor config: diff --git a/scaninfo-datastore/build.gradle.kts b/scaninfo-datastore/build.gradle.kts index b516f07bf..b45583f93 100644 --- a/scaninfo-datastore/build.gradle.kts +++ b/scaninfo-datastore/build.gradle.kts @@ -26,4 +26,4 @@ dependencies { testImplementation("org.mockito:mockito-core:${Version.mockito}") } -apply(from = "../gradle/jacoco.gradle") \ No newline at end of file +apply(from = "../gradle/jacoco.gradle.kts") \ No newline at end of file diff --git a/scaninfo-shredder/Dockerfile b/scaninfo-shredder/Dockerfile index 7ed4f0288..bd04156a0 100644 --- a/scaninfo-shredder/Dockerfile +++ b/scaninfo-shredder/Dockerfile @@ -1,4 +1,4 @@ -FROM azul/zulu-openjdk:12.0.2 +FROM azul/zulu-openjdk:13 LABEL maintainer="dev@redotter.sg" diff --git a/scaninfo-shredder/Dockerfile.test b/scaninfo-shredder/Dockerfile.test index 2d4c4ad2a..d47fc2f81 100644 --- a/scaninfo-shredder/Dockerfile.test +++ b/scaninfo-shredder/Dockerfile.test @@ -1,4 +1,4 @@ -FROM azul/zulu-openjdk:12.0.2 +FROM azul/zulu-openjdk:13 LABEL maintainer="dev@redotter.sg" diff --git a/scaninfo-shredder/build.gradle.kts b/scaninfo-shredder/build.gradle.kts index 4593b1668..451fca29c 100644 --- a/scaninfo-shredder/build.gradle.kts +++ b/scaninfo-shredder/build.gradle.kts @@ -53,4 +53,4 @@ tasks.register("version") { } } -apply(from = "../gradle/jacoco.gradle") \ No newline at end of file +apply(from = "../gradle/jacoco.gradle.kts") diff --git a/seagull/README.md b/seagull/README.md index df5b48428..41c6da9e1 100644 --- a/seagull/README.md +++ b/seagull/README.md @@ -4,6 +4,13 @@ A Dockerized version of [Seagull](http://gull.sourceforge.net/ "Seagull") - a mu Based on https://github.com/codeghar/Seagull +#### Build image + +```` +docker build -t seagull . +```` + + ####To test with this Image with docker-compose * Use the docker-compose file for seagull to start Prime and OCS-gw diff --git a/secure-archive/build.gradle.kts b/secure-archive/build.gradle.kts index 80314f7df..e801c7987 100644 --- a/secure-archive/build.gradle.kts +++ b/secure-archive/build.gradle.kts @@ -19,4 +19,4 @@ dependencies { testImplementation(kotlin("test-junit")) } -apply(from = "../gradle/jacoco.gradle") \ No newline at end of file +apply(from = "../gradle/jacoco.gradle.kts") \ No newline at end of file diff --git a/sim-administration/es2plus4dropwizard/build.gradle.kts b/sim-administration/es2plus4dropwizard/build.gradle.kts index 5310dbf4e..22561d5c5 100644 --- a/sim-administration/es2plus4dropwizard/build.gradle.kts +++ b/sim-administration/es2plus4dropwizard/build.gradle.kts @@ -17,4 +17,4 @@ dependencies { testImplementation("org.mockito:mockito-core:${Version.mockito}") } -apply(from = "../../gradle/jacoco.gradle") \ No newline at end of file +apply(from = "../../gradle/jacoco.gradle.kts") \ No newline at end of file diff --git a/sim-administration/hss-adapter/build.gradle.kts b/sim-administration/hss-adapter/build.gradle.kts index fdfece202..ce45b2637 100644 --- a/sim-administration/hss-adapter/build.gradle.kts +++ b/sim-administration/hss-adapter/build.gradle.kts @@ -100,7 +100,7 @@ protobuf { protobufGeneratedFilesBaseDir = generatedFilesBaseDir } -apply(from = "../../gradle/jacoco.gradle") +apply(from = "../../gradle/jacoco.gradle.kts") idea { module { diff --git a/sim-administration/jersey-json-schema-validator/build.gradle.kts b/sim-administration/jersey-json-schema-validator/build.gradle.kts index f148894e9..9b9c45e1c 100644 --- a/sim-administration/jersey-json-schema-validator/build.gradle.kts +++ b/sim-administration/jersey-json-schema-validator/build.gradle.kts @@ -9,9 +9,9 @@ dependencies { implementation(kotlin("stdlib-jdk8")) implementation("io.dropwizard:dropwizard-core:${Version.dropwizard}") - implementation("com.github.everit-org.json-schema:org.everit.json.schema:1.11.1") + implementation("com.github.everit-org.json-schema:org.everit.json.schema:1.12.0") testImplementation("io.dropwizard:dropwizard-testing:${Version.dropwizard}") } -apply(from = "../../gradle/jacoco.gradle") \ No newline at end of file +apply(from = "../../gradle/jacoco.gradle.kts") \ No newline at end of file diff --git a/sim-administration/ostelco-dropwizard-utils/build.gradle.kts b/sim-administration/ostelco-dropwizard-utils/build.gradle.kts index 747437a94..9dc256d31 100644 --- a/sim-administration/ostelco-dropwizard-utils/build.gradle.kts +++ b/sim-administration/ostelco-dropwizard-utils/build.gradle.kts @@ -29,4 +29,4 @@ dependencies { testImplementation("io.dropwizard:dropwizard-testing:${Version.dropwizard}") } -apply(from = "../../gradle/jacoco.gradle") \ No newline at end of file +apply(from = "../../gradle/jacoco.gradle.kts") \ No newline at end of file diff --git a/sim-administration/sim-batch-management/.gitignore b/sim-administration/sim-batch-management/.gitignore new file mode 100644 index 000000000..c97f963b3 --- /dev/null +++ b/sim-administration/sim-batch-management/.gitignore @@ -0,0 +1 @@ +*.sh diff --git a/sim-administration/uploader/README.md b/sim-administration/sim-batch-management/README-upload-sim-batch.md similarity index 95% rename from sim-administration/uploader/README.md rename to sim-administration/sim-batch-management/README-upload-sim-batch.md index d535bb6cf..26c815894 100644 --- a/sim-administration/uploader/README.md +++ b/sim-administration/sim-batch-management/README-upload-sim-batch.md @@ -1,6 +1,6 @@ # How to upload batch information to prime using the -## Introuduction +## Introduction Prime has REST endpoint for uploading sim batches. This is an interface with little error checking (beyond the bare miniumums) and a low abstraction layer: It requires a CSV file of ICCID/IMSI/MSISDN/PROFILE tuples. @@ -59,6 +59,8 @@ and will cause error messages.) ``` + -batch-length integer + The number of profiles in the batch. Must match with iccid, msisdn and imsi ranges (if present). -first-iccid string An 18 or 19 digit long string. The 19-th digit being a luhn luhnChecksum digit, if present (default "not a valid iccid") -first-imsi string diff --git a/sim-administration/README.md b/sim-administration/sim-batch-management/README.md similarity index 100% rename from sim-administration/README.md rename to sim-administration/sim-batch-management/README.md diff --git a/sim-administration/sim-batch-management/TODO.md b/sim-administration/sim-batch-management/TODO.md new file mode 100644 index 000000000..aa3827208 --- /dev/null +++ b/sim-administration/sim-batch-management/TODO.md @@ -0,0 +1,21 @@ +An informal TODO list for the sim batch management tool +== + +1. Make an RDBMS that handles sim card workflows. +2. It must by necessity be able to handle free lists of + imsis, msisdns etc. +3. As today, it should be possible to -generate- those lists + from parameters, where that makes sense. In general howeve,r + in particular for production use, this will not be the case + and we need to cater for that. +4. The programme should -initially- be wholly command line + oriented, with a database using sqlite. +5. At some (much) later stage, it may make sense to put it + in some cloud, somewhere. +6. The interfaces to external parties will be + - input/output files for profile generation. + - some kind of file (not yet determind) for msisdn lists. + - HTTP upload commands, either indirectly via curl (as now), or + directly from the script later. In either case + it will be assumed that tunnels are set up out of band, and + tunnel setup is not part of this program. diff --git a/sim-administration/sim-batch-management/loltelutils/loltelutils.go b/sim-administration/sim-batch-management/loltelutils/loltelutils.go new file mode 100644 index 000000000..c8b91d204 --- /dev/null +++ b/sim-administration/sim-batch-management/loltelutils/loltelutils.go @@ -0,0 +1,24 @@ +package loltelutils + + +func TrimSuffix(s string, suffixLen int) string { + return s[:len(s)-suffixLen] +} + +func Sign(x int) int { + if x < 0 { + return -1 + } else if x > 0 { + return 1 + } else { + return 0 + } +} + +// Abs returns the absolute value of x. +func Abs(x int) int { + if x < 0 { + return -x + } + return x +} diff --git a/sim-administration/sim-batch-management/outfile_to_hss_input_converter.go b/sim-administration/sim-batch-management/outfile_to_hss_input_converter.go new file mode 100755 index 000000000..f94fc345c --- /dev/null +++ b/sim-administration/sim-batch-management/outfile_to_hss_input_converter.go @@ -0,0 +1,51 @@ +//usr/bin/env go run "$0" "$@"; exit "$?" +/** + * This program is intended to be used from the command line, and will convert an + * output file from a sim card vendor into an input file for a HSS. The assumptions + * necessary for this to work are: + * + * * The SIM card vendor produces output files similar to the example .out file + * found in the same source directory as this program + * + * * The HSS accepts input as a CSV file, with header line 'ICCID, IMSI, KI' and subsequent + * lines containing ICCID/IMSI/Ki fields, all separated by commas. + * + * Needless to say, the outmost care should be taken when handling Ki values and + * this program must, as a matter of course, be considered a security risk, as + * must all software that touch SIM values. + * + * With that caveat in place, the usage of this program typically looks like + * this: + * + * ./outfile_to_hss_input_converter.go \ + * -input-file sample_out_file_for_testing.out + * -output-file-prefix ./hss-input-for- + * + * (followed by cryptographically strong erasure of the .out file, + * encapsulation of the .csv file in strong cryptography etc., none + * of which are handled by this script). + */ + +package main + +import ( + "fmt" + "github.com/ostelco/ostelco-core/sim-administration/sim-batch-management/outfileconversion" + "log" +) + +func main() { + inputFile, outputFilePrefix := outfileconversion.ParseOutputToHssConverterCommandLine() + + fmt.Println("inputFile = ", inputFile) + fmt.Println("outputFilePrefix = ", outputFilePrefix) + + outRecord := outfileconversion.ReadOutputFile(inputFile) + outputFile := outputFilePrefix + outRecord.OutputFileName + ".csv" + fmt.Println("outputFile = ", outputFile) + + err := outfileconversion.WriteHssCsvFile(outputFile, outRecord.Entries) + if err != nil { + log.Fatal("Couldn't close output file '", outputFilePrefix, "'. Error = '", err, "'") + } +} diff --git a/sim-administration/uploader/outfile_to_hss_input_converter.go b/sim-administration/sim-batch-management/outfileconversion/outfile_to_hss_input_converter_lib.go old mode 100755 new mode 100644 similarity index 73% rename from sim-administration/uploader/outfile_to_hss_input_converter.go rename to sim-administration/sim-batch-management/outfileconversion/outfile_to_hss_input_converter_lib.go index fb5ef5186..6e53238ff --- a/sim-administration/uploader/outfile_to_hss_input_converter.go +++ b/sim-administration/sim-batch-management/outfileconversion/outfile_to_hss_input_converter_lib.go @@ -1,37 +1,10 @@ -//usr/bin/env go run "$0" "$@"; exit "$?" -/** - * This program is intended to be used from the command line, and will convert an - * output file from a sim card vendor into an input file for a HSS. The assumptions - * necessary for this to work are: - * - * * The SIM card vendor produces output files similar to the example .out file - * found in the same source directory as this program - * - * * The HSS accepts input as a CSV file, with header line 'ICCID, IMSI, KI' and subsequent - * lines containing ICCID/IMSI/Ki fields, all separated by commas. - * - * Needless to say, the outmost care should be taken when handling Ki values and - * this program must, as a matter of course, be considered a security risk, as - * must all software that touch SIM values. - * - * With that caveat in place, the usage of this program typically looks like - * this: - * - * ./outfile_to_hss_input_converter.go \ - * -input-file sample_out_file_for_testing.out - * -output-file-prefix ./hss-input-for- - * - * (followed by cryptographically strong erasure of the .out file, - * encapsulation of the .csv file in strong cryptography etc., none - * of which are handled by this script). - */ - -package main +package outfileconversion import ( "bufio" "flag" "fmt" + "github.com/ostelco/ostelco-core/sim-administration/sim-batch-management/loltelutils" "log" "os" "regexp" @@ -39,26 +12,6 @@ import ( "strings" ) -/// -/// Main. -/// - -func main() { - inputFile, outputFilePrefix := parseCommandLine() - - fmt.Println("inputFile = ", inputFile) - fmt.Println("outputFilePrefix = ", outputFilePrefix) - - outRecord := ReadOutputFile(inputFile) - outputFile := outputFilePrefix + outRecord.outputFileName + ".csv" - fmt.Println("outputFile = ", outputFile) - - err := WriteHssCsvFile(outputFile, outRecord.entries) - if err != nil { - log.Fatal("Couldn't close output file '", outputFilePrefix, "'. Error = '", err, "'") - } -} - /// /// Data structures /// @@ -67,16 +20,16 @@ type OutputFileRecord struct { Filename string inputVariables map[string]string headerDescription map[string]string - entries []SimEntry - // TODO: As it is today, the noOfEntries is just the number of entries, - // but I may want to change that to be the declared number of entries, - // and then later, dynamically, read in the individual entries + Entries []SimEntry + // TODO: As it is today, the noOfEntries is just the number of Entries, + // but I may want to change that to be the declared number of Entries, + // and then later, dynamically, read in the individual Entries // in a channel that is just piped to the goroutine that writes - // them to file, and fails if the number of declared entries - // differs from the actual number of entries. .... but that is + // them to file, and fails if the number of declared Entries + // differs from the actual number of Entries. .... but that is // for another day. noOfEntries int - outputFileName string + OutputFileName string } const ( @@ -107,6 +60,23 @@ type ParserState struct { /// Functions /// + +func ParseOutputToHssConverterCommandLine() (string, string) { + inputFile := flag.String("input-file", + "not a valid filename", + "path to .out file used as input file") + + outputFile := flag.String("output-file-prefix", + "not a valid filename", + "prefix to path to .csv file used as input file, filename will be autogenerated") + + // + // Parse input according to spec above + // + flag.Parse() + return *inputFile, *outputFile +} + func ParseLineIntoKeyValueMap(line string, theMap map[string]string) { var splitString = strings.Split(line, ":") if len(splitString) != 2 { @@ -117,10 +87,6 @@ func ParseLineIntoKeyValueMap(line string, theMap map[string]string) { theMap[key] = value } -func trimSuffix(s string, suffixLen int) string { - return s[:len(s)-suffixLen] -} - func ReadOutputFile(filename string) OutputFileRecord { _, err := os.Stat(filename) @@ -193,10 +159,10 @@ func ReadOutputFile(filename string) OutputFileRecord { iccidWithChecksum := rawIccid if strings.HasSuffix(rawIccid, "F") { - iccidWithChecksum = trimSuffix(rawIccid, 1) + iccidWithChecksum = loltelutils.TrimSuffix(rawIccid, 1) } - var iccidWithoutChecksum = trimSuffix(iccidWithChecksum, 1) + var iccidWithoutChecksum = loltelutils.TrimSuffix(iccidWithChecksum, 1) // TODO: Enable this!! checkICCIDSyntax(iccidWithChecksum) entry := SimEntry{ rawIccid: rawIccid, @@ -235,9 +201,9 @@ func ReadOutputFile(filename string) OutputFileRecord { Filename: filename, inputVariables: state.inputVariables, headerDescription: state.headerDescription, - entries: state.entries, + Entries: state.entries, noOfEntries: declaredNoOfEntities, - outputFileName: getOutputFileName(state), + OutputFileName: getOutputFileName(state), } return result @@ -295,31 +261,7 @@ func isComment(s string) bool { return match } -/// XXX Add a main function that -// a) Reads the output file, then produces a HSS input file from that -// b) Later, integrate with the prime input generator, and add a -// database to keep track of the workflow. -// -// - -// -// Set up command line parsing -// -func parseCommandLine() (string, string) { - inputFile := flag.String("input-file", - "not a valid filename", - "path to .out file used as input file") - outputFile := flag.String("output-file-prefix", - "not a valid filename", - "prefix to path to .csv file used as input file, filename will be autogenerated") - - // - // Parse input according to spec above - // - flag.Parse() - return *inputFile, *outputFile -} // fileExists checks if a file exists and is not a directory before we // try using it to prevent further errors. @@ -360,3 +302,4 @@ func WriteHssCsvFile(filename string, entries []SimEntry) error { fmt.Println("Successfully written ", max, " sim card records.") return f.Close() } + diff --git a/sim-administration/uploader/outfile_to_hss_input_converter_test.go b/sim-administration/sim-batch-management/outfileconversion/outfile_to_hss_input_converter_lib_test.go similarity index 90% rename from sim-administration/uploader/outfile_to_hss_input_converter_test.go rename to sim-administration/sim-batch-management/outfileconversion/outfile_to_hss_input_converter_lib_test.go index df1e4fad9..0d6a9bda7 100644 --- a/sim-administration/uploader/outfile_to_hss_input_converter_test.go +++ b/sim-administration/sim-batch-management/outfileconversion/outfile_to_hss_input_converter_lib_test.go @@ -1,4 +1,4 @@ -package main +package outfileconversion import ( "gotest.tools/assert" @@ -14,7 +14,7 @@ func testKeywordValueParser(t *testing.T) { func testReadOutputFile(t *testing.T) { sample_output_file_name := "sample_out_file_for_testing.out" - record, _ := ReadOutputFile(sample_output_file_name) + record := ReadOutputFile(sample_output_file_name) // First parameter to check assert.Equal(t, sample_output_file_name, record.Filename) @@ -31,6 +31,6 @@ func testReadOutputFile(t *testing.T) { assert.Equal(t, record.inputVariables["IMSI"], "242017100011213") // Check that the output entry set looks legit. - assert.Equal(t, 3, len(record.entries)) + assert.Equal(t, 3, len(record.Entries)) assert.Equal(t, 3, record.noOfEntries) } diff --git a/sim-administration/uploader/sample_out_file_for_testing.out b/sim-administration/sim-batch-management/sample_out_file_for_testing.out similarity index 100% rename from sim-administration/uploader/sample_out_file_for_testing.out rename to sim-administration/sim-batch-management/sample_out_file_for_testing.out diff --git a/sim-administration/sim-batch-management/upload-sim-batch.go b/sim-administration/sim-batch-management/upload-sim-batch.go new file mode 100755 index 000000000..71e5b93b2 --- /dev/null +++ b/sim-administration/sim-batch-management/upload-sim-batch.go @@ -0,0 +1,15 @@ +//usr/bin/env go run "$0" "$@"; exit "$?" + +package main + +import "github.com/ostelco/ostelco-core/sim-administration/sim-batch-management/uploadtoprime" + +func main() { + batch := uploadtoprime.ParseUploadFileGeneratorCommmandline() + + // TODO: Combine these two into something inside uploadtoprime. + // It's unecessary to break the batch thingy open in this way. + var csvPayload = uploadtoprime.GenerateCsvPayload(batch) + + uploadtoprime.GeneratePostingCurlscript(batch.Url, csvPayload) +} \ No newline at end of file diff --git a/sim-administration/uploader/luhn_test.go b/sim-administration/sim-batch-management/uploadtoprime/luhn_test.go similarity index 97% rename from sim-administration/uploader/luhn_test.go rename to sim-administration/sim-batch-management/uploadtoprime/luhn_test.go index da3055935..524771436 100644 --- a/sim-administration/uploader/luhn_test.go +++ b/sim-administration/sim-batch-management/uploadtoprime/luhn_test.go @@ -1,4 +1,4 @@ -package main +package uploadtoprime import ( diff --git a/sim-administration/sim-batch-management/uploadtoprime/upload-sim-batch-lib-test.go b/sim-administration/sim-batch-management/uploadtoprime/upload-sim-batch-lib-test.go new file mode 100644 index 000000000..e1d249b45 --- /dev/null +++ b/sim-administration/sim-batch-management/uploadtoprime/upload-sim-batch-lib-test.go @@ -0,0 +1,30 @@ +package uploadtoprime + +import ( + "fmt" + "gotest.tools/assert" + "testing" +) + +func TestParseInputFileGeneratorCommmandline(t *testing.T) { + + parsedBatch := ParseInputFileGeneratorCommmandline() + assert.Equal(t, "Footel", parsedBatch.customer) + assert.Equal(t, "BAR_FOOTEL_STD", parsedBatch.profileType) + assert.Equal(t, "20191007", parsedBatch.orderDate) + assert.Equal(t, "2019100701", parsedBatch.batchNo) + assert.Equal(t, 10, parsedBatch.quantity) + assert.Equal(t, 894700000000002214, parsedBatch.firstIccid) + assert.Equal(t, 242017100012213, parsedBatch.firstImsi) +} + +// TODO: Make a test that checks that the correct number of things are made, +// and also that the right things are made. Already had one fencepost error +// on this stuff. + +func TestGenerateInputFile(t *testing.T) { + parsedBatch := ParseInputFileGeneratorCommmandline() + var result = GenerateInputFile(parsedBatch) + + fmt.Println(result) +} diff --git a/sim-administration/uploader/upload-sim-batch.go b/sim-administration/sim-batch-management/uploadtoprime/upload-sim-batch-lib.go similarity index 67% rename from sim-administration/uploader/upload-sim-batch.go rename to sim-administration/sim-batch-management/uploadtoprime/upload-sim-batch-lib.go index 4d916bcde..bdb069639 100755 --- a/sim-administration/uploader/upload-sim-batch.go +++ b/sim-administration/sim-batch-management/uploadtoprime/upload-sim-batch-lib.go @@ -8,34 +8,25 @@ // considered technical debt, and the debt can be paid back e.g. by // internalizing the logic into prime. -package main +package uploadtoprime import ( "flag" "fmt" + "github.com/ostelco/ostelco-core/sim-administration/sim-batch-management/loltelutils" "log" "net/url" "regexp" + "strconv" "strings" ) -import ( - . "strconv" -) - -func main() { - batch := parseCommandLine() - var csvPayload string = generateCsvPayload(batch) - - generatePostingCurlscript(batch.url, csvPayload) -} - -func generatePostingCurlscript(url string, payload string) { +func GeneratePostingCurlscript(url string, payload string) { fmt.Printf("#!/bin/bash\n") fmt.Printf("curl -H 'Content-Type: text/plain' -X PUT --data-binary @- %s < -1; i-- { - t, _ := ParseInt(source[i], 10, 8) + t, _ := strconv.ParseInt(source[i], 10, 8) n := int(t) if double { @@ -72,10 +63,10 @@ func calculateChecksum(luhnString string, double bool) int { } func LuhnChecksum(number int) int { - return generateControlDigit(Itoa(number)) + return generateControlDigit(strconv.Itoa(number)) } -func generateCsvPayload(batch Batch) string { +func GenerateCsvPayload(batch OutputBatch) string { var sb strings.Builder sb.WriteString("ICCID, IMSI, MSISDN, PIN1, PIN2, PUK1, PUK2, PROFILE\n") @@ -83,7 +74,7 @@ func generateCsvPayload(batch Batch) string { var imsi = batch.firstImsi var msisdn = batch.firstMsisdn - for i := 0; i <= batch.length; i++ { + for i := 0; i < batch.length; i++ { iccid := fmt.Sprintf("%d%1d", iccidWithoutLuhnChecksum, LuhnChecksum(iccidWithoutLuhnChecksum)) line := fmt.Sprintf("%s, %d, %d,,,,,%s\n", iccid, imsi, msisdn, batch.profileType) @@ -155,9 +146,9 @@ func checkProfileType(name string, potentialProfileName string) { } } -type Batch struct { +type OutputBatch struct { profileType string - url string + Url string length int firstMsisdn int msisdnIncrement int @@ -168,32 +159,10 @@ type Batch struct { } func IccidWithoutLuhnChecksum(s string) string { - return trimSuffix(s, 1) -} - -func trimSuffix(s string, suffixLen int) string { - return s[:len(s)-suffixLen] -} - -func Sign(x int) int { - if x < 0 { - return -1 - } else if x > 0 { - return 1 - } else { - return 0 - } -} - -// Abs returns the absolute value of x. -func Abs(x int) int { - if x < 0 { - return -x - } - return x + return loltelutils.TrimSuffix(s, 1) } -func parseCommandLine() Batch { +func ParseUploadFileGeneratorCommmandline() OutputBatch { // // Set up command line parsing @@ -209,6 +178,10 @@ func parseCommandLine() Batch { firstMsisdn := flag.String("first-msisdn", "Not a valid MSISDN", "First MSISDN in batch") lastMsisdn := flag.String("last-msisdn", "Not a valid MSISDN", "Last MSISDN in batch") profileType := flag.String("profile-type", "Not a valid sim profile type", "SIM profile type") + batchLengthString := flag.String( + "batch-quantity", + "Not a valid batch-quantity, must be an integer", + "Number of sim cards in batch") // XXX Legal values are Loltel and M1 at this time, how to configure that // flexibly? Eventually by puttig them in a database and consulting it during @@ -246,6 +219,15 @@ func parseCommandLine() Batch { checkMSISDNSyntax("last-msisdn", *lastMsisdn) checkMSISDNSyntax("first-msisdn", *firstMsisdn) + batchLength, err := strconv.Atoi(*batchLengthString) + if err != nil { + log.Fatalf("Not a valid batch quantity string '%s'.\n", *batchLengthString) + } + + if batchLength <= 0 { + log.Fatalf("OutputBatch quantity must be positive, but was '%d'", batchLength) + } + uploadUrl := fmt.Sprintf("http://%s:%s/ostelco/sim-inventory/%s/import-batch/profilevendor/%s?initialHssState=%s", *uploadHostname, *uploadPortnumber, *hssVendor, *profileVendor, *initialHlrActivationStatusOfProfiles) @@ -253,28 +235,34 @@ func parseCommandLine() Batch { checkProfileType("profile-type", *profileType) // Convert to integers, and get lengths + msisdnIncrement := -1 + if *firstMsisdn <= *lastMsisdn { + msisdnIncrement = 1 + } - log.Println("firstmsisdn =", *firstMsisdn) - log.Println("lastmsisdn =", *lastMsisdn) + log.Println("firstmsisdn = ", *firstMsisdn) + log.Println("lastmsisdn = ", *lastMsisdn) + log.Println("msisdnIncrement = ", msisdnIncrement) - var firstMsisdnInt, _ = Atoi(*firstMsisdn) - var lastMsisdnInt, _ = Atoi(*lastMsisdn) + var firstMsisdnInt, _ = strconv.Atoi(*firstMsisdn) + var lastMsisdnInt, _ = strconv.Atoi(*lastMsisdn) var msisdnLen = lastMsisdnInt - firstMsisdnInt + 1 if msisdnLen < 0 { msisdnLen = -msisdnLen } - var firstImsiInt, _ = Atoi(*firstIMSI) - var lastImsiInt, _ = Atoi(*lastIMSI) + var firstImsiInt, _ = strconv.Atoi(*firstIMSI) + var lastImsiInt, _ = strconv.Atoi(*lastIMSI) var imsiLen = lastImsiInt - firstImsiInt + 1 - var firstIccidInt, _ = Atoi(IccidWithoutLuhnChecksum(*firstIccid)) - var lastIccidInt, _ = Atoi(IccidWithoutLuhnChecksum(*lastIccid)) + var firstIccidInt, _ = strconv.Atoi(IccidWithoutLuhnChecksum(*firstIccid)) + var lastIccidInt, _ = strconv.Atoi(IccidWithoutLuhnChecksum(*lastIccid)) var iccidlen = lastIccidInt - firstIccidInt + 1 // Validate that lengths of sequences are equal in absolute // values. - if Abs(msisdnLen) != Abs(iccidlen) || Abs(msisdnLen) != Abs(imsiLen) { + // TODO: Perhaps use some varargs trick of some sort here? + if loltelutils.Abs(msisdnLen) != loltelutils.Abs(iccidlen) || loltelutils.Abs(msisdnLen) != loltelutils.Abs(imsiLen) || batchLength != loltelutils.Abs(imsiLen) { log.Printf("msisdnLen = %10d\n", msisdnLen) log.Printf("iccidLen = %10d\n", iccidlen) log.Printf("imsiLen = %10d\n", imsiLen) @@ -287,15 +275,57 @@ func parseCommandLine() Batch { } // Return a correctly parsed batch - return Batch{ + return OutputBatch{ profileType: *profileType, - url: uploadUrl, - length: Abs(iccidlen), + Url: uploadUrl, + length: loltelutils.Abs(iccidlen), firstIccid: firstIccidInt, - iccidIncrement: Sign(iccidlen), + iccidIncrement: loltelutils.Sign(iccidlen), firstImsi: firstImsiInt, - imsiIncrement: Sign(imsiLen), + imsiIncrement: loltelutils.Sign(imsiLen), firstMsisdn: firstMsisdnInt, - msisdnIncrement: Sign(msisdnLen), + msisdnIncrement: msisdnIncrement, } } + +/// +/// Input batch management +/// + +type InputBatch struct { + customer string + profileType string + orderDate string + batchNo string + quantity int + firstIccid int + firstImsi int +} + +func ParseInputFileGeneratorCommmandline() InputBatch { + // TODO: This function should be rewritten to parse a string array and send it to flags. + // we need to up our Go-Fu before we can make flag.Parse(arguments) work + + return InputBatch{customer: "Footel", profileType: "BAR_FOOTEL_STD", orderDate: "20191007", batchNo: "2019100701", quantity: 10, firstIccid: 894700000000002214, firstImsi: 242017100012213} +} + +func GenerateInputFile(batch InputBatch) string { + result := "*HEADER DESCRIPTION\n" + + "***************************************\n" + + fmt.Sprintf("Customer :%s\n", batch.customer) + + fmt.Sprintf("ProfileType : %s\n", batch.profileType) + + fmt.Sprintf("Order Date : %s\n", batch.orderDate) + + fmt.Sprintf("Batch No : %s\n", batch.batchNo) + + fmt.Sprintf("Quantity : %d\n", batch.quantity) + + "***************************************\n" + + "*INPUT VARIABLES\n" + + "***************************************\n" + + "var_In:\n" + + fmt.Sprintf(" ICCID: %d\n", batch.firstIccid) + + fmt.Sprintf("IMSI: %d\n", batch.firstImsi) + + "***************************************\n" + + "*OUTPUT VARIABLES\n" + + "***************************************\n" + + "var_Out: ICCID/IMSI/KI\n" + return result +} diff --git a/sim-administration/simcard-utils/build.gradle.kts b/sim-administration/simcard-utils/build.gradle.kts index ccc84137f..01e127e0c 100644 --- a/sim-administration/simcard-utils/build.gradle.kts +++ b/sim-administration/simcard-utils/build.gradle.kts @@ -11,4 +11,4 @@ dependencies { testImplementation("io.dropwizard:dropwizard-testing:${Version.dropwizard}") } -apply(from = "../../gradle/jacoco.gradle") \ No newline at end of file +apply(from = "../../gradle/jacoco.gradle.kts") \ No newline at end of file diff --git a/sim-administration/simmanager/build.gradle.kts b/sim-administration/simmanager/build.gradle.kts index 62d637dd2..9a143c007 100644 --- a/sim-administration/simmanager/build.gradle.kts +++ b/sim-administration/simmanager/build.gradle.kts @@ -72,6 +72,12 @@ dependencies { testImplementation("io.dropwizard:dropwizard-testing:${Version.dropwizard}") testImplementation("com.nhaarman.mockitokotlin2:mockito-kotlin:${Version.mockitoKotlin}") + testImplementation("net.bytebuddy:byte-buddy:${Version.byteBuddy}") { + because("mockito-kotlin:2.2.0 has byte-buddy:1.9.0 which does not work for java13") + } + testImplementation("net.bytebuddy:byte-buddy-agent:${Version.byteBuddy}") { + because("mockito-kotlin:2.2.0 has byte-buddy:1.9.0 which does not work for java13") + } testImplementation("org.testcontainers:postgresql:${Version.testcontainers}") testImplementation(project(":sim-administration:sm-dp-plus-emulator")) @@ -100,6 +106,10 @@ val integration = tasks.create("integration", Test::class.java) { tasks.build.get().dependsOn(integration) +jacoco { + toolVersion = "0.8.3" // because 0.8.4 has a issue - https://github.com/mockito/mockito/issues/1717 +} + var protobufGeneratedFilesBaseDir: String = "" protobuf { @@ -119,7 +129,7 @@ protobuf { protobufGeneratedFilesBaseDir = generatedFilesBaseDir } -apply(from = "../../gradle/jacoco.gradle") +apply(from = "../../gradle/jacoco.gradle.kts") idea { module { @@ -131,4 +141,4 @@ idea { tasks.clean { delete(protobufGeneratedFilesBaseDir) -} +} \ No newline at end of file diff --git a/sim-administration/simmanager/src/main/kotlin/org/ostelco/simcards/admin/SmdpPlusHealthceck.kt b/sim-administration/simmanager/src/main/kotlin/org/ostelco/simcards/admin/SmdpPlusHealthceck.kt index aaab1bbbb..5ea086b22 100644 --- a/sim-administration/simmanager/src/main/kotlin/org/ostelco/simcards/admin/SmdpPlusHealthceck.kt +++ b/sim-administration/simmanager/src/main/kotlin/org/ostelco/simcards/admin/SmdpPlusHealthceck.kt @@ -121,15 +121,17 @@ class SmdpPlusHealthceck( // the endpoint in the other end actually gave a reasonable answer to a reasonable request, // indicating that the endpoint is answering requests, then continue to loop over next endpoint, // otherwise see if there is an error. - when (pingResult) { + return when (pingResult) { is Either.Left -> if (pingResult.a.pingOk) { - return true + true } else { - logger.error("Could not reach SM-DP+ via HTTP PING:", pingResult) - return false + pingResult.mapLeft { error -> + logger.error("Could not reach SM-DP+ via HTTP PING: '${error.description}'. '$error'") + } + false } is Either.Right -> { - return true + true } } } diff --git a/sim-administration/simmanager/src/main/kotlin/org/ostelco/simcards/profilevendors/ProfileVendorAdapterDatum.kt b/sim-administration/simmanager/src/main/kotlin/org/ostelco/simcards/profilevendors/ProfileVendorAdapterDatum.kt index 170e889f0..8fdc404fe 100644 --- a/sim-administration/simmanager/src/main/kotlin/org/ostelco/simcards/profilevendors/ProfileVendorAdapterDatum.kt +++ b/sim-administration/simmanager/src/main/kotlin/org/ostelco/simcards/profilevendors/ProfileVendorAdapterDatum.kt @@ -138,11 +138,13 @@ data class ProfileVendorAdapter( .flatMap { response -> if (executionWasFailure(status = response.myHeader.functionExecutionStatus)) { logAndReturnNotFoundError("execution getProfileStatusA. iccidList='$iccidList', status=${response.myHeader.functionExecutionStatus}") - } else if (response.profileStatusList == null) { - logAndReturnNotFoundError("Couldn't find any response for query iccidlist='$iccidList'") } else { - val result = response.profileStatusList!! // TODO: Why is this necessary (see if-branch above) - return result.right() + val result = response.profileStatusList + if (result == null) { + logAndReturnNotFoundError("Couldn't find any response for query iccidlist='$iccidList'") + } else { + result.right() + } } } } @@ -181,7 +183,7 @@ data class ProfileVendorAdapter( return NotUpdatedError("simEntry without id. simEntry=$simEntry").left() } - + return confirmOrderA(iccid = simEntry.iccid, eid = eid, releaseFlag = releaseFlag) .flatMap { response -> @@ -220,6 +222,7 @@ data class ProfileVendorAdapter( .flatMap { it.first().right() } + /** * Downloads the SM-DP+ 'profile status' information for a list of ICCIDs * from a SM-DP+ service. @@ -267,12 +270,12 @@ data class ProfileVendorAdapter( * A dummy ICCID. May or may notreturn a valid profile from any HSS or SM-DP+, but is * useful for checking of there is an SM-DP+ in the other end of the connection. */ - val invalidICCID = listOf("8901000000000000001") + val listContainingOnlyInvalidIccid = listOf("8901000000000000001") /** * Contact the ES2+ endpoint of the SM-DP+, and return true if the answer indicates * that it's up. */ fun ping(): Either> = - getProfileStatus(iccidList = invalidICCID, expectSuccess = false) + getProfileStatus(iccidList = listContainingOnlyInvalidIccid, expectSuccess = false) } \ No newline at end of file diff --git a/sim-administration/sm-dp-plus-emulator/Dockerfile b/sim-administration/sm-dp-plus-emulator/Dockerfile index 4abd863bc..6e139c8fc 100644 --- a/sim-administration/sm-dp-plus-emulator/Dockerfile +++ b/sim-administration/sm-dp-plus-emulator/Dockerfile @@ -1,4 +1,4 @@ -FROM azul/zulu-openjdk:12.0.2 +FROM azul/zulu-openjdk:13 LABEL maintainer="dev@redotter.sg" diff --git a/sim-administration/sm-dp-plus-emulator/build.gradle.kts b/sim-administration/sm-dp-plus-emulator/build.gradle.kts index d1c858103..eab37890d 100644 --- a/sim-administration/sm-dp-plus-emulator/build.gradle.kts +++ b/sim-administration/sm-dp-plus-emulator/build.gradle.kts @@ -44,4 +44,4 @@ tasks.withType { archiveVersion.set("") } -apply(from = "../../gradle/jacoco.gradle") \ No newline at end of file +apply(from = "../../gradle/jacoco.gradle.kts") \ No newline at end of file diff --git a/sim-administration/uploader/.gitignore b/sim-administration/uploader/.gitignore deleted file mode 100644 index 129c50361..000000000 --- a/sim-administration/uploader/.gitignore +++ /dev/null @@ -1 +0,0 @@ -generate-digi-profiles.sh diff --git a/slack/build.gradle.kts b/slack/build.gradle.kts index 95ca400ac..bc9b06390 100644 --- a/slack/build.gradle.kts +++ b/slack/build.gradle.kts @@ -35,4 +35,4 @@ tasks.test { } } -apply(from = "../gradle/jacoco.gradle") \ No newline at end of file +apply(from = "../gradle/jacoco.gradle.kts") \ No newline at end of file diff --git a/tools/prime-admin/config/config.yaml b/tools/prime-admin/config/config.yaml index e9aeb6aad..ed83e752c 100644 --- a/tools/prime-admin/config/config.yaml +++ b/tools/prime-admin/config/config.yaml @@ -7,16 +7,26 @@ modules: config: host: neo4j protocol: bolt - hssNameLookupService: - serviceInterface: org.ostelco.prime.storage.graph.HssNameLookupService - textReader: - type: classpathResource - filename: /HssNameLookupService.kts onNewCustomerAction: serviceInterface: org.ostelco.prime.storage.graph.OnNewCustomerAction textReader: type: classpathResource filename: /OnNewCustomerAction.kts + allowedRegionsService: + serviceInterface: org.ostelco.prime.storage.graph.AllowedRegionsService + textReader: + type: classpathResource + filename: /AllowedRegionsService.kts + onRegionApprovedAction: + serviceInterface: org.ostelco.prime.storage.graph.OnRegionApprovedAction + textReader: + type: classpathResource + filename: /OnRegionApprovedAction.kts + hssNameLookupService: + serviceInterface: org.ostelco.prime.storage.graph.HssNameLookupService + textReader: + type: classpathResource + filename: /HssNameLookupService.kts # - type: sim-manager # config: # hlrs: [] diff --git a/tools/prime-admin/src/main/kotlin/org/ostelco/tools/prime/admin/Main.kt b/tools/prime-admin/src/main/kotlin/org/ostelco/tools/prime/admin/Main.kt index 3691b8d07..a1730b088 100644 --- a/tools/prime-admin/src/main/kotlin/org/ostelco/tools/prime/admin/Main.kt +++ b/tools/prime-admin/src/main/kotlin/org/ostelco/tools/prime/admin/Main.kt @@ -1,7 +1,16 @@ package org.ostelco.tools.prime.admin import org.ostelco.prime.PrimeApplication +import org.ostelco.tools.prime.admin.actions.addCustomerToSegment +import org.ostelco.tools.prime.admin.actions.approveRegionForCustomer +import org.ostelco.tools.prime.admin.actions.createCustomer +import org.ostelco.tools.prime.admin.actions.createSubscription +import org.ostelco.tools.prime.admin.actions.getAllRegionDetails +import org.ostelco.tools.prime.admin.actions.print +import org.ostelco.tools.prime.admin.actions.printLeft +import org.ostelco.tools.prime.admin.actions.setBalance import org.ostelco.tools.prime.admin.modules.DwEnvModule +import kotlin.math.pow /** * Update `config/config.yaml` to point to valid Neo4j store and Postgres. @@ -11,7 +20,7 @@ import org.ostelco.tools.prime.admin.modules.DwEnvModule fun main() { PrimeApplication().run("server", "config/config.yaml") try { - doActions() + setupCustomer() println("Done") } finally { DwEnvModule.env.applicationContext.server.stop() @@ -20,37 +29,96 @@ fun main() { } } -fun doActions() { +fun setupCustomer() { -// check() -// sync() -// setup() -// index() + val email = "" + val nickname = "" + val regionCode = "" + val segmentId = "" + val iccId = "" + val alias = "" + val msisdn = "" -// createCustomer(email = "", nickname = "").printLeft() + createCustomer(email = email, nickname = nickname).printLeft() // deleteCustomer(email = "").printLeft() + // set bundle balance + setBalance(email = email, balance = 10 * 2.0.pow(30.0).toLong()).printLeft() + + // check balance + // link to region -// approveRegionForCustomer(email = "", regionCode = "").printLeft() + approveRegionForCustomer(email = email, regionCode = regionCode).printLeft() + + // link to segment + addCustomerToSegment(email = email, segmentId = segmentId).printLeft() // add SimProfile -// createSubscription( -// email = "", -// regionCode = "", -// alias = "", -// msisdn = "").printLeft() + createSubscription( + email = email, + regionCode = regionCode, + alias = alias, + msisdn = msisdn, + iccId = iccId).printLeft() // remove SimProfile + // Get region details +// getRegionDetails(email = email, regionCode = regionCode).print() + + // Get all region details + getAllRegionDetails(email = email).print() +} + +fun batchProvision() { + + val email = "" + val nickname = "" + + createCustomer(email = email, nickname = nickname).printLeft() + // set bundle balance -// setBalance(email = "", balance = 10 * 2.0.pow(30.0).toLong()).printLeft() + setBalance(email = email, balance = 10 * 2.0.pow(30.0).toLong()).printLeft() - // check bundle balance + // check balance - // Get region details -// getRegionDetails(email = "", regionCode = "").print() + val data = mapOf( + "" to listOf( + SimProfileData(iccId = "", msisdn = "") + ) + ) + + for (regionCode in data.keys) { + + // link to region + approveRegionForCustomer(email = email, regionCode = regionCode).printLeft() + + for (index in 0..9) { + + val (iccId, msisdn) = data[regionCode]?.get(index) ?: throw Exception() + + // add SimProfile + createSubscription( + email = email, + regionCode = regionCode, + alias = "SIM ${index + 1} for $regionCode", + msisdn = msisdn, + iccId = iccId).printLeft() + + } + } // Get all region details -// getAllRegionDetails(email = "").print() + getAllRegionDetails(email = email).print() +} + +fun doActions() { + +// check() +// sync() +// setup() +// index() + +} -} \ No newline at end of file +data class SimProfileData(val iccId: String, val msisdn: String) \ No newline at end of file diff --git a/tools/prime-admin/src/main/kotlin/org/ostelco/tools/prime/admin/actions/CustomerActions.kt b/tools/prime-admin/src/main/kotlin/org/ostelco/tools/prime/admin/actions/CustomerActions.kt index d086fbac4..343e3d658 100644 --- a/tools/prime-admin/src/main/kotlin/org/ostelco/tools/prime/admin/actions/CustomerActions.kt +++ b/tools/prime-admin/src/main/kotlin/org/ostelco/tools/prime/admin/actions/CustomerActions.kt @@ -7,16 +7,15 @@ import arrow.core.left import arrow.core.right import arrow.effects.IO import arrow.instances.either.monad.monad +import org.ostelco.prime.dsl.withId import org.ostelco.prime.dsl.writeTransaction import org.ostelco.prime.model.Bundle import org.ostelco.prime.model.Customer import org.ostelco.prime.model.Identity -import org.ostelco.prime.model.Segment import org.ostelco.prime.storage.NotFoundError import org.ostelco.prime.storage.StoreError import org.ostelco.prime.storage.ValidationError import org.ostelco.prime.storage.graph.adminStore -import org.ostelco.prime.storage.graph.getSegmentNameFromCountryCode import java.util.* // @@ -25,11 +24,7 @@ import java.util.* fun createCustomer(email: String, nickname: String): Either = adminStore .addCustomer( - identity = Identity( - id = email, - type = "EMAIL", - provider = "email" - ), + identity = emailAsIdentity(email = email), customer = Customer( id = UUID.randomUUID().toString(), nickname = nickname, @@ -37,46 +32,14 @@ fun createCustomer(email: String, nickname: String): Either = analyticsId = UUID.randomUUID().toString(), referralId = UUID.randomUUID().toString())) -fun assignCustomerToRegionSegment(email: String, regionCode: String): Either = IO { - Either.monad().binding { - - val customerId = adminStore.getCustomer( - identity = Identity( - id = email, - type = "EMAIL", - provider = "email" - ) - ) - .bind() - .id - - adminStore.updateSegment( - segment = Segment( - id = getSegmentNameFromCountryCode(regionCode), - subscribers = listOf(customerId) - ) - ) - .bind() - }.fix() -}.unsafeRunSync() - fun approveRegionForCustomer(email: String, regionCode: String): Either = IO { Either.monad().binding { - val customerId = adminStore.getCustomer( - identity = Identity(id = email, type = "EMAIL", provider = "email") - ) + val customerId = adminStore + .getCustomer(identity = emailAsIdentity(email = email)) .bind() .id - adminStore.updateSegment( - segment = Segment( - id = getSegmentNameFromCountryCode(regionCode), - subscribers = listOf(customerId) - ) - ) - .bind() - adminStore.approveRegionForCustomer( customerId = customerId, regionCode = regionCode @@ -85,14 +48,20 @@ fun approveRegionForCustomer(email: String, regionCode: String): Either = writeTransaction { + IO { + Either.monad().binding { + val customerId = adminStore + .getCustomer(identity = emailAsIdentity(email = email)) + .bind() + .id + fact { (Customer withId customerId) belongsToSegment (org.ostelco.prime.storage.graph.model.Segment withId segmentId) }.bind() + }.fix() + }.unsafeRunSync() +} + fun deleteCustomer(email: String) = adminStore - .removeCustomer( - identity = Identity( - id = email, - type = "EMAIL", - provider = "email" - ) - ) + .removeCustomer(identity = emailAsIdentity(email = email)) fun createSubscription( email: String, @@ -101,11 +70,7 @@ fun createSubscription( alias: String = "", msisdn: String) = adminStore .addSubscription( - identity = Identity( - id = email, - type = "EMAIL", - provider = "email" - ), + identity = emailAsIdentity(email = email), regionCode = regionCode, iccId = iccId, alias = alias, @@ -117,13 +82,7 @@ fun createSubscription( // fun setBalance(email: String, balance: Long) = adminStore - .getBundles( - identity = Identity( - id = email, - type = "EMAIL", - provider = "email" - ) - ) + .getBundles(identity = emailAsIdentity(email = email)) .flatMap { bundles -> when (bundles.size) { 0 -> NotFoundError( @@ -154,21 +113,17 @@ fun setBalance(email: String, balance: Long) = adminStore // fun getAllRegionDetails(email: String) = adminStore - .getAllRegionDetails( - identity = Identity( - id = email, - type = "EMAIL", - provider = "email" - ) - ) + .getAllRegionDetails(identity = emailAsIdentity(email = email)) fun getRegionDetails(email: String, regionCode: String) = adminStore .getRegionDetails( - identity = Identity( - id = email, - type = "EMAIL", - provider = "email" - ), + identity = emailAsIdentity(email = email), regionCode = regionCode ) +// Common +private fun emailAsIdentity(email: String) = Identity( + id = email, + type = "EMAIL", + provider = "email" +) \ No newline at end of file diff --git a/tools/prime-admin/src/main/kotlin/org/ostelco/tools/prime/admin/actions/SystemActions.kt b/tools/prime-admin/src/main/kotlin/org/ostelco/tools/prime/admin/actions/SystemActions.kt index 3062d97a7..90c7a1c36 100644 --- a/tools/prime-admin/src/main/kotlin/org/ostelco/tools/prime/admin/actions/SystemActions.kt +++ b/tools/prime-admin/src/main/kotlin/org/ostelco/tools/prime/admin/actions/SystemActions.kt @@ -4,18 +4,20 @@ import org.ostelco.prime.kts.engine.reader.ClasspathResourceTextReader import org.ostelco.prime.kts.engine.script.RunnableKotlinScript +private val scriptBaseDir = "" + fun setup() { - RunnableKotlinScript(ClasspathResourceTextReader("/Setup.kts").readText()).eval() + RunnableKotlinScript(ClasspathResourceTextReader("$scriptBaseDir/Setup.kts").readText()).eval() } fun sync() { - RunnableKotlinScript(ClasspathResourceTextReader("/Sync.kts").readText()).eval() + RunnableKotlinScript(ClasspathResourceTextReader("$scriptBaseDir/Sync.kts").readText()).eval() } fun check() { - RunnableKotlinScript(ClasspathResourceTextReader("/Check.kts").readText()).eval() + RunnableKotlinScript(ClasspathResourceTextReader("$scriptBaseDir/Check.kts").readText()).eval() } fun index() { - RunnableKotlinScript(ClasspathResourceTextReader("/Index.kts").readText()).eval() -} \ No newline at end of file + RunnableKotlinScript(ClasspathResourceTextReader("$scriptBaseDir/Index.kts").readText()).eval() +} diff --git a/tools/prime-admin/src/main/kotlin/org/ostelco/tools/prime/admin/actions/Utils.kt b/tools/prime-admin/src/main/kotlin/org/ostelco/tools/prime/admin/actions/Utils.kt index c95cdd9e9..f492eae6a 100644 --- a/tools/prime-admin/src/main/kotlin/org/ostelco/tools/prime/admin/actions/Utils.kt +++ b/tools/prime-admin/src/main/kotlin/org/ostelco/tools/prime/admin/actions/Utils.kt @@ -2,6 +2,7 @@ package org.ostelco.tools.prime.admin.actions import arrow.core.Either import org.ostelco.prime.jsonmapper.objectMapper +import org.ostelco.prime.storage.StoreError val formatJson = objectMapper.writerWithDefaultPrettyPrinter()::writeValueAsString @@ -10,7 +11,11 @@ fun formatJson(json: String): String = objectMapper .let(formatJson) fun Either.printLeft() = this.mapLeft { left -> - println(left) + if (left is StoreError) { + println(left.message) + } else { + println(left) + } left } diff --git a/tracing/build.gradle.kts b/tracing/build.gradle.kts index 08471e6b3..6e8ace845 100644 --- a/tracing/build.gradle.kts +++ b/tracing/build.gradle.kts @@ -31,4 +31,4 @@ tasks.test { } } -apply(from = "../gradle/jacoco.gradle") \ No newline at end of file +apply(from = "../gradle/jacoco.gradle.kts") \ No newline at end of file